Lombik
A practical Flask scaffold engine
Lombik is a practical scaffold engine for Flask that saves you from hours of configuration, integrations, boilerplate, and project-structure decisions.
It follows a hypermedia-first approach, leaning heavily on Flask, Jinja2, HTMX, Tailwind CSS, and server-rendered HTML. The idea is simple: keep as much application logic as possible close to the UI, without introducing a heavy frontend framework unless you actually need one.
As a real-world example, our commercial product proov — a digital delivery note / proof-of-delivery platform for logistics — is built with Lombik.
The application is roughly 20K lines of code, with HTML making up more than half of it:
Language Files Lines Extension
--------------------------------------------------------
HTML 58 11622 .html
Python 87 7871 .py
JavaScript 6 957 .js
Markdown 6 281 .md
CSS 1 276 .css
Text 3 238 .txt
Shell Script 1 146 .sh
JSON 1 37 .json
XML 1 22 .xml
TOML 1 2 .toml
--------------------------------------------------------
TOTAL: 165 21452
========================================================
The goal of Lombik is not to be the biggest Flask framework ever created.
It's to remove repetitive work and give you a clean starting point for building real applications.
Getting started
1. Install Lombik
pip install lombik
I try to keep dependencies to a minimum.
Currently Lombik installs:
- Flask>=3.0
- Flask-SQLAlchemy>=3.1
- Flask-Migrate>=4.0
- Flask-Session>=0.6
- Flask-WTF>=1.2
- Flask-Caching>=2.3
- Flask-Limiter>=3.8
- SQLAlchemy>=2.0
- python-dotenv>=1.0
- pytest>=7.1.0
- pytest-cov>=7.1.0
- click>=8.1
- resend
- python-dateutil>=2.9.0
2. Create your application
Navigate to the folder where you want your application and run:
lombik createapp myapp
This generates the complete project structure for you.
Once created, you can run the application with:
lombik run
Before doing that, however, you'll probably want to initialize the database and create your first superuser.
3. Initialize the database
lombik initdb
By default, the development/test environment uses SQLite.
Production is configured to use MySQL by default, but you can change this in:
lombik/configuration.py
Make sure your database credentials are configured in your environment variables.
4. Create a superuser
lombik superuser
Now run the application:
lombik run
And you're ready to go.
Routes
Lombik keeps routing deliberately simple.
Let's say we want a page that only authenticated users can access.
At the top of your module:
from lombik.wrappers import login_required
Then create the route:
@core_bp.route("/members")
@login_required
def members():
return render_template("...")
Simple.
Now let's make another route that only administrators and superusers can access.
from lombik.wrappers import login_required, roles_required
@core_bp.route("/admin")
@login_required
@roles_required("admin", "superuser")
def admin():
return render_template("...")
Keep admin functionality separate
I don't recommend mixing admin functionality into your core application.
Create a dedicated module instead:
lombik module admin
This generates an admin blueprint with the default structure and automatically registers it for you.
Keeps things tidy, which future-you will appreciate.
Rate limiting
Lombik also includes an in-memory rate limiter.
For production, you may want to configure a shared backend such as Redis depending on your deployment setup.
from lombik.extensions import limiter
@core_bp.route("/admin")
@limiter.limit("60 per minute")
@login_required
@roles_required("admin", "superuser")
def admin():
return render_template("...")
Actions
Lombik encourages keeping routes, actions, and queries separate.
An action is something that changes application state.
For example, let's allow users to change their timezone.
When a user signs up, Lombik defaults their timezone to UTC.
1. Pass the available timezones to the template
Lombik includes a list of timezones in:
lombik/constants.py
Import it:
from lombik.constants import TIMEZONES
Then pass it into your page context:
@core_bp.route("/members")
def members():
context = {
"selected": "members",
"timezones": TIMEZONES,
}
return render_template(
"core/members.html",
**context
)
Python unpacks the dictionary, so the template can access it as:
{{ timezones }}
2. Create the UI
Create:
core/members.html
Then use Jinja and HTMX to create the timezone selector:
{% extends "base/base.html" %}
{% block title %}
Members
{% endblock %}
{% block content %}
<div class="space-y-6">
<h1>Hello {{ g.user.username }}</h1>
<select
hx-patch="{{ url_for('core_bp.update_timezone') }}"
hx-vals='{
"csrf_token": "{{ csrf_token() }}"
}'
hx-trigger="change"
hx-target="#timezoneUpdateError"
hx-swap="innerHTML"
name="tz">
{% for tz in timezones %}
<option
value="{{ tz }}"
{% if g.user.timezone == tz %}
selected
{% endif %}
>
{{ tz }}
</option>
{% endfor %}
</select>
<div id="timezoneUpdateError"></div>
</div>
{% endblock %}
You could use forms.py for this, but for a small action like this I personally prefer keeping it lightweight.
3. Create the action
Create the action in:
blueprints/core/actions.py
from flask import g, request
from db import db
from lombik.responses import Result, htmx_response
from lombik.users import change_user_timezone
from . import core_bp
@core_bp.patch("/users/me/timezone")
def update_timezone():
def _timezone_message(message, color):
return htmx_response(
html=f"""
<button
onclick="this.classList.add('hidden');"
class="text-xs -mt-2 flex items-center gap-1 {color}">
{message}
<ion-icon name="close-circle-outline"></ion-icon>
</button>
"""
)
new_timezone = request.values.get("tz", "")
result = change_user_timezone(
user_id=g.user.id,
new_timezone=new_timezone,
)
return _timezone_message(
message=result.message,
color="green" if result.success else "red",
)
That's it.
The built-in change_user_timezone() function handles validation and returns a Result object.
Lombik uses Result objects as the standard way to represent action outcomes.
The structure is intentionally simple:
Result(
success=True,
data={"new_timezone": new_timezone},
message="Timezone changed successfully.",
)
data can also be None when the action fails.
Template filters
Lombik includes several filters designed to make server-rendered UI easier to work with.
You'll find them in:
lombik/filters.py
proper
Capitalizes words and replaces underscores with spaces.
{{ john_doe | proper }}
Becomes:
John Doe
possessive
Turns a name into a possessive form:
{{ john_doe | proper | possessive }}
Becomes:
John Doe's
So you can write:
{% block title %}
{{ g.user.full_name | proper | possessive }} dashboard
{% endblock %}
timesince
Lombik also includes human-readable time filters.
Instead of displaying:
2026-08-19 13:33:44.763467
You can display:
<p>
You became a member {{ g.user.created_at | timesince }}
</p>
Which could result in:
You became a member 5 hours ago
The filter progresses naturally:
just now
→ few minutes ago
→ N minutes ago
→ N hours ago
→ N days ago
→ ...
For future dates, use:
{{ some_date | timeuntil }}
There are several other useful filters included with Lombik. Check lombik/filters.py for the complete list.
Queries
Queries are deliberately separated from routes and actions.
As a general rule, resources should be queried through dedicated query functions unless doing so would make the code unnecessarily complicated.
For example:
@core_bp.get("/users")
def get_users():
status = request.args.get("status")
return get_users_by_status(status=status)
You can combine this with Lombik's built-in cache:
from lombik.extensions import cache
@core_bp.get("/users")
@cache.memoize(timeout=30)
def get_users():
status = request.args.get("status")
users = get_users_by_status(status=status)
return render_template(
"core/partials/users.html",
users=users,
)
Create the partial:
core/partials/users.html
{% for user in users %}
<li>{{ user.username | proper }}</li>
{% endfor %}
And load it dynamically with HTMX:
<div>
<p>Active members</p>
<ul
hx-get="{{ url_for('core_bp.get_users', status='active') }}"
hx-trigger="load, every 30s">
<!-- HTMX populates this -->
</ul>
</div>
No frontend framework required.
Models
Let's say we want to turn our application into a multi-tenant platform.
We'll need a tenants table.
Instead of creating the model manually, use Lombik's model generator:
lombik model tenant
Use singular names when generating models.
Lombik will use the singular name for the Python class and generate the plural table name automatically.
The command creates the model and registers it in:
models/__init__.py
A generated model looks roughly like this:
from db import db
import uuid
from lombik.utils import utc_now
class Tenant(db.Model):
__tablename__ = "tenants"
id = db.Column(
db.String(36),
primary_key=True,
default=lambda: str(uuid.uuid4())
)
user_id = db.Column(
db.String(36),
db.ForeignKey("users.id"),
unique=True
)
name = db.Column(
db.String(255)
)
created_at = db.Column(
db.DateTime(timezone=True),
default=utc_now
)
# <LOMBIK:RELATIONSHIPS>
Add your own columns, but don't remove the relationship marker.
For example, update your User model with:
tenant_id = db.Column(
db.String(36)
)
In production, you'd probably make this nullable=False and recreate your superuser.
Migrations
When you're done:
lombik db -m "added tenants"
This creates the migration and upgrades the database in one command.
Relationships
Lombik also includes a relationship generator:
lombik relate parent.field to child.field [one-to-many|many-to-one|one-to-one|many-to-many] [--lazy LAZY]
For example:
lombik relate tenant.id to user.tenant_id one-to-many
lombik relate user.tenant_id to tenant.id many-to-one
lombik relate tenant.id to setting.tenant_id one-to-one
lombik relate user.id to role.user_id many-to-many
The relationships are inserted into both models below the Lombik relationship marker.
For example:
lombik relate tenant.id to user.tenant_id one-to-many
UI
Lombik comes with a lightweight theme handler in:
static/js/theme.js
It supports light and dark mode and works nicely with Tailwind's built-in dark: utilities.
Theme toggle
<button
onclick="toggleDarkMode()"
id="changeThemeBtn">
<ion-icon
id="themeIcon"
name="moon-outline">
</ion-icon>
<span id="themeText">
Dark mode
</span>
</button>
Dropdowns
I always thought HTML should have a simpler way of doing dropdowns.
So Lombik has one.
Use:
<dropdown></dropdown>
and put links or buttons inside it.
When using a button to open a dropdown, set:
type="button"
Otherwise, it can interfere with form submission.
The parent element should also be positioned relatively.
Example:
<div class="relative">
<button type="button">
<ion-icon name="settings-outline"></ion-icon>
</button>
<dropdown class="absolute right-0 w-40">
<a href="/">Home</a>
<a href="/apis">API</a>
<a href="/settings">Settings</a>
<hr class="my-1 dark:border-darkaccent" />
<button
type="button"
onclick="toggleDarkMode()"
id="changeThemeBtn">
<ion-icon
id="themeIcon"
name="moon-outline">
</ion-icon>
<span id="themeText">
Dark mode
</span>
</button>
<hr class="my-1" />
<button
type="button"
hx-post="{{ url_for('auth_bp.logout') }}"
hx-vals='{
"csrf_token": "{{ csrf_token() }}"
}'>
Log out
</button>
</dropdown>
</div>
Style it however you like.
Drag & drop
Lombik also includes a lightweight drag-and-drop implementation in:
static/js/drag.js
For the full API and examples, see the documentation inside that file.
The main idea is simple:
<dragarea>
...
</dragarea>
Anything with the class:
.dragable
or:
.draggable
can be dragged.
Connecting multiple drag areas
For things such as task boards:
<dragarea family="tasks">
Every <dragarea> using the same family can interact with each other.
Saving state
Each draggable item needs a unique ID:
<div
class="draggable"
data-id="{{ task.id }}">
Then attach HTMX to the drag area:
<dragarea
family="tasks"
hx-patch="/update-task-status"
hx-vals='{
"csrf_token": "{{ csrf_token() }}"
}'
field="status"
value="open">
Lombik will automatically send information about the dragged item, including:
{
"id": item.dataset.id,
"status": "open",
"from_index": 0,
"to_index": 1,
"from_field": "status",
"from_value": "new"
}
Free dragging
Another custom element is:
<dragfree></dragfree>
This allows elements to be moved freely without snapping into a board.
You can persist the location by giving the element an ID:
<dragfree id="note-{{ note.id }}">
...
</dragfree>
The position is stored in localStorage.
Double-click the element to reset its position.
Charts
Lombik comes with a built in library that makes charts easy.
It supports
- bar charts
- area charts
- bubble charts
- line charts
- donut charts
Creating one is simple:
<bar-chart
x="['HTML', 'JS', 'Python', 'CSS']"
y="[1000, 2000, 1500, 800]"
x-title="Language"
y-title="LOC"
>
</bar-chart>
What you can also do isntead of separate x-y values, is to pass a dictionary intt the data attribute like this:
<bar-chart
data='{
"HTML": 1000,
"JS": 2000,
"Python": 1500,
"CSS": 800
}'
x-title="Language"
y-title="LOC"
>
</bar-chart>
Area and line chart both work the same way.
Donut chart:
<donut-chart
labels="['HTML', 'JS', 'Python', 'CSS']"
values="[1000, 2000, 1500, 800]"
legend="true"
>
</donut-chart>
OR
<donut-chart data='{
"HTML": 1000,
"JS": 2000,
"Python": 1500,
"CSS": 800
}'></donut-chart>
Bubble chart:
<bubble-chart
x='[1, 2, 3, 4, 5]'
y='[10, 15, 8, 20, 12]'
size='[40, 80, 30, 100, 60]'
labels='["A", "B", "C", "D", "E"]'
x_title="X Axis"
y_title="Y Axis"
theme="ocean"
></bubble-chart>
OR
<bubble-chart
data='{"A": {"x": 1, "y": 10, "size": 40}, "B": {"x": 2, "y": 15, "size": 80}}'
x_title="X"
y_title="Y"
></bubble-chart>
You might have noticed, bubble-chart had an attribute called theme=""
You can use this to change any chart's theme colors.
They are stored in static/js/chart.js
The current themes were all generated with AI, I encourage you to create your own themes matching you app.
Other useful features
Lombik contains a bunch of smaller utilities that are easy to overlook.
Error handling
During development, you may want to disable or comment out the custom exception handler in:
lombik/errors.py
This lets Flask's debug error screen show normally.
Alternatively, you can expose the exception in your 500.html template during development.
Flash messages
Flash messages have their own class and can be used across routes.
from lombik.flash import Flash
Then:
Flash.ok("Profile updated successfully.")
Flash.error("Something went wrong.")
There are several message types available. Check the Flash class for the full API.
Messages are automatically injected into the base template and come with styling, icons, and animations.
Modals
Opening a modal is deliberately simple:
<button onclick="openModal('myModal')">
Open modal
</button>
Lombik handles opening, closing, and clicking outside the modal.
A default modal template is included at:
base/partials/modal.html
Testing
Lombik uses standard Flask/Pytest testing with a few convenience commands:
lombik test
Run the test suite.
lombik test_report
Run tests and generate a report.
lombik test_report_html
Run tests and generate an HTML report.
Sessions
When CSRF/session tokens expire, Lombik can show the user a dedicated page prompting them to refresh their session.
The expiry time can be configured through the application configuration.
Resend is configured as the default email provider.
Add your API key to your environment and send email with:
from lombik.mail import send_email
Forms
Lombik includes a lightweight form-validation system.
Have a look at the default login and registration pages to see how it works.
The goal is intentionally not to build another giant form framework. Keep the validation close to the form and keep it readable.
Images
Image saving and compression utilities are available in:
lombik/images
Validation
Reusable validation helpers live in:
lombik/validation
They include things such as:
- email validation
- role validation
- password strength validation
- and other common validators
Final thoughts
There is more to Lombik, but you'll probably discover most of it as you build.
It's a small engine, but I like to think its useful.
At least for the applications I build with it.
The project is MIT licensed, so contributions, fixes, ideas, and forks are more than welcome.
Hopefully Lombik saves you a few hours of boilerplate and lets you get to the interesting part of building your application a little faster.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lombik-3.2.2.tar.gz.
File metadata
- Download URL: lombik-3.2.2.tar.gz
- Upload date:
- Size: 1.8 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5189b0a114def38a5b5765ff6e4a4a7d5890a9c5d90f4c8a8e1539460350beb3
|
|
| MD5 |
141c7fbfbaa4e8f88dbbdb819d1d59fb
|
|
| BLAKE2b-256 |
881641dee58f4a7e6282d3d5b8478da7cac4250b33b02a215d405cf3c981758e
|
File details
Details for the file lombik-3.2.2-py3-none-any.whl.
File metadata
- Download URL: lombik-3.2.2-py3-none-any.whl
- Upload date:
- Size: 1.8 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a7a8e5e8a342cca8bfd5f05c51a4c75f94e4601afe25ce5d1fca34b17831d29a
|
|
| MD5 |
1c0acb630784dd2fc6eab80e3786ad70
|
|
| BLAKE2b-256 |
b27b6c8355398f576a80ce2ff2a4db5fd533e53049686fe07ac657c9f1456db7
|