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.
pip install lombikI 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
Navigate to the folder where you want your application and run:
lombik createapp myappThis generates the complete project structure for you.
Once created, you can run the application with:
lombik runBefore doing that, however, you'll probably want to initialize the database and create your first superuser.
lombik initdbBy 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.
lombik superuserNow run the application:
lombik runAnd you're ready to go.
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_requiredThen 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("...")I don't recommend mixing admin functionality into your core application.
Create a dedicated module instead:
lombik module adminThis generates an admin blueprint with the default structure and automatically registers it for you.
Keeps things tidy, which future-you will appreciate.
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("...")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.
Lombik includes a list of timezones in:
lombik/constants.py
Import it:
from lombik.constants import TIMEZONESThen 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 }}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.
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.
Lombik includes several filters designed to make server-rendered UI easier to work with.
You'll find them in:
lombik/filters.py
Capitalizes words and replaces underscores with spaces.
{{ john_doe | proper }}Becomes:
John Doe
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 %}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 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.
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 tenantUse 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.
When you're done:
lombik db -m "added tenants"This creates the migration and upgrades the database in one command.
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-manyThe relationships are inserted into both models below the Lombik relationship marker.
For example:
lombik relate tenant.id to user.tenant_id one-to-manyLombik 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.
<button
onclick="toggleDarkMode()"
id="changeThemeBtn">
<ion-icon
id="themeIcon"
name="moon-outline">
</ion-icon>
<span id="themeText">
Dark mode
</span>
</button>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.
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.
For things such as task boards:
<dragarea family="tasks">Every <dragarea> using the same family can interact with each other.
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"
}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.
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.
Lombik contains a bunch of smaller utilities that are easy to overlook.
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 have their own class and can be used across routes.
from lombik.flash import FlashThen:
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.
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
Lombik uses standard Flask/Pytest testing with a few convenience commands:
lombik testRun the test suite.
lombik test_reportRun tests and generate a report.
lombik test_report_htmlRun tests and generate an HTML report.
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_emailLombik 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.
Image saving and compression utilities are available in:
lombik/images
Reusable validation helpers live in:
lombik/validation
They include things such as:
- email validation
- role validation
- password strength validation
- and other common validators
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.