Project structure
Every Saaslivery app, core or third-party, uses the same layout and the same split of responsibilities. The structure is a contract: automated checks at submission time and code review both assume it.
Prerequisites
- Python 3.11+
- The Heaven framework (
pip install heaven) - Basic familiarity with async Python and Jinja2
- HTMX and Alpine.js knowledge helps; both are served by the platform, nothing to install
The layout
my-app/
├── __init__.py # Empty, makes it a Python package
├── manifest.py # Identity, dependencies, events, permissions, pricing
├── plugin.py # Heaven App() + route registration
├── schema.py # pytastic TypedDict schemas (input + output)
├── helpers.py # Constants, row converters, shared query logic
├── apis.py # API handlers (JSON responses)
├── pages.py # UI pages + HTMX partials (HTML responses)
├── authz.py # AUTHZ dict + authorization functions
├── events.py # Event bus handlers (optional)
├── forms.py # Form definitions (optional)
├── events.yml # Emitted actions + subscriptions (if the app uses events)
├── migrations/ # Database schema, max 10 tables
│ └── 001_create_my_app_tables.sql
├── templates/ # Flat; the TEMPLATES prefix namespaces them
│ ├── index.html
│ └── partials/
└── assets/
└── style.css # App-specific CSS, served at /assets/*
One file, one job
| File | Returns | Never contains |
|---|---|---|
pages.py |
HTML via res.render() |
Business logic for mutations |
apis.py |
JSON via res.out() |
HTML strings |
helpers.py |
Shared logic for both | Route registrations |
authz.py |
Authorization decisions | Anything else |
There is never a single handlers.py. If a function renders HTML it belongs in pages.py; if it returns JSON it belongs in apis.py; if both need it, it belongs in helpers.py.
Key rules
| Rule | Why |
|---|---|
| Max 10 database tables | Keeps apps small; decompose over the event bus if you need more |
workspace_id on every table |
The platform enforces workspace isolation |
App(debug=False) |
The platform owns debug and livereload |
No ASSETS(), CORS, or sessions in plugin.py |
The platform registers these globally |
Templates flat, namespaced by prefix |
app.TEMPLATES("templates", relative_to=__file__, prefix="my-app") |
UI on the wildcard subdomain under /app/{slug}/ |
See Architecture & routing |
API routes namespaced /{slug}/* on api |
Prevents collisions on the shared API subdomain |
| Type-annotated handlers | (req: Request, res: Response, ctx: Context), always |
HTTPStatus constants |
Never magic numbers |
| Imports at module level | Never inside functions |
| Events for all cross-app communication | Never import from or query another app |
How apps are discovered
The platform scans apps/ (core) and plugins/ (third-party) at startup for directories containing plugin.py. For each one it:
- Imports the module and mounts the exported
appobject - Reads
manifest.pyfor the slug, events, and permission catalogue - Registers
/public/*(platform assets) and/assets/*(your assets) - Registers the app's
events.ymlactions and subscriptions on the event bus - Merges the app's
AUTHZdict into the platform authorization gate
Your app does nothing special to be discovered. Follow the structure, export app from plugin.py, and the platform does the rest.
Next steps
- Your first app, a complete walkthrough
- App manifest, the full field reference
- Building forms, the declarative form system