What is an app?
A Saaslivery app is a small, self-contained productivity tool that does one thing well. It has its own tab in the workspace shell, its own database tables, its own templates, and its own manifest. It plugs into the platform as a Heaven plugin and gets auth, sessions, tenancy, permissions, and asset serving for free.
The micro philosophy
Traditional productivity suites are monoliths: one giant app that does everything. Saaslivery takes the opposite approach.
- Tasks manages boards and assignments. It doesn't know about leave requests.
- Leaves manages time off. It doesn't know about calendars.
- Calendar manages events. It doesn't know about mail.
Each app is simple on its own. Complexity comes from connecting apps through the event bus, not from bloating individual apps. When leave is approved, the Calendar app hears about it and blocks the dates. When a task is completed, any listening app can react.
The 10-table rule
Every app owns a maximum of 10 database tables. This is a hard constraint, checked at review time.
If your app needs more than 10 tables, it is doing too much. Decompose it into smaller apps that communicate via events.
| Guideline | Detail |
|---|---|
| Maximum owned tables | 10 per app |
| Junction tables | Count toward the limit |
| Projection tables | Local read models of another app's events do not count, but follow the projection rules |
workspace_id column |
Required on every table |
| Enforcement | Validated at app review time |
App structure
Every app follows the same directory layout; the full tour is in Project structure:
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
├── helpers.py # Constants, row converters, shared logic
├── apis.py # API handlers (JSON responses)
├── pages.py # UI pages + HTMX partials (HTML responses)
├── authz.py # AUTHZ dict + authorization functions
├── events.py # Event handlers (optional)
├── forms.py # Form definitions (optional)
├── migrations/ # Database schema (max 10 tables)
├── templates/ # Flat; namespaced by the TEMPLATES prefix
└── assets/ # App-specific CSS, images (served at /assets/*)
Key rules
- Self-contained. No app imports from another app's directory. Cross-app communication goes through the event bus only.
- Split handlers. HTML in
pages.py, JSON inapis.py, shared logic inhelpers.py, authorization inauthz.py. Never a singlehandlers.py. - Flat, prefixed templates.
app.TEMPLATES("templates", relative_to=__file__, prefix="my-app")namespaces template names without nesting directories. - No debug mode. Child apps must use
App(debug=False); the platform owns debug tooling. - No ASSETS, CORS, or sessions. The platform registers these globally.
Apps are Heaven plugins
The platform auto-discovers apps at startup. It scans apps/ (core) and plugins/ (third-party) for directories containing a plugin.py, imports them, and mounts them.
from heaven import App
app = App(debug=False)
app.TEMPLATES("templates", relative_to=__file__, prefix="tasks")
# UI: {workspace}.saaslivery.com/app/tasks/*
ws = app.subdomain("*")
ws.GET("/app/tasks/", "apps.tasks.pages.index")
ws.GET("/app/tasks/board/:board_id", "apps.tasks.pages.board")
# HTMX partials: same wildcard subdomain
ws.GET("/app/tasks/partials/task-list", "apps.tasks.pages.task_list_partial")
# API: api.saaslivery.com/tasks/*
api = app.subdomain("api")
api.GET("/tasks", "apps.tasks.apis.api_list_tasks")
api.POST("/tasks", "apps.tasks.apis.api_create_task")
When mounted, the app gets:
| Feature | Provided by |
|---|---|
A tab in the workspace shell at /app/{slug}/ |
Platform auto-discovery |
| Shared sessions across all subdomains | Platform session middleware |
| CORS on the API subdomain | Platform CORS handler |
/public/* platform assets |
Platform asset registration |
/assets/* app-specific assets |
Platform asset registration |
ctx.workspace_id on every request |
Platform tenant middleware |
| Permission loading + the authz gate | Platform security middleware |
Core apps vs. third-party apps
| Aspect | Core apps | Third-party apps |
|---|---|---|
| Location | apps/ directory |
plugins/ directory (git submodule) |
| Publisher | The platform | Developer's registered publisher ID |
| Event namespace | core.{app}.* |
ext.{publisher}.{app}.* |
| Permission keys | {slug}.* |
ext.{publisher}.{slug}.* |
| Review | Internal | Platform team reviews code before deploy |
| Pricing | Included in workspace tiers | Set by the developer (80/20 split) |
Both follow the exact same structure, rules, and isolation guarantees.
What an app cannot do
| Action | Why not |
|---|---|
| Import from another app | Use the event bus instead |
Set ctx.workspace_id |
Platform-controlled, read-only |
| Register its own sessions | Platform handles sessions globally |
| Register CORS | Platform handles CORS on the API subdomain |
Use App(debug=True) |
Causes livereload collision on mount |
| Own more than 10 tables | Decompose into smaller apps |
| Use a JavaScript build step | HTMX + Alpine.js only |
Next steps
- App manifest, declare your app's identity, events, and pricing
- Your first app, build a working app from scratch
- Event system, how apps communicate