Start here
Concepts
Box
One Linux server running Tiffin: a managed box that ShipTiffin sets up in your Hetzner
account, or one you make with tiffin up (quickstart). It holds any
number of projects.
Project and resources
A project is described by tiffin.config.ts. Tiffin turns it into resources:
app/web, service/postgres, bucket/uploads, env/LOG_LEVEL, cron/nightly and so
on. Each resource has a live state on the machine: pending, ready or failed.
The dashboard's names map to services: Database is postgres, KV (key-value,
Redis-compatible, also a cache) is valkey, Files is storage, then auth, email and
analytics; Jobs are the top-level queues and crons. database, cache and files also work in tiffin.config.ts and are stored
under the first name (which is what tiffin pull writes back).
Every project always has a Database, KV, Files (with a private bucket files), Email,
Analytics and Jobs. An empty one costs next to nothing (a database and role in the shared
cluster, a key prefix, a storage account, a password) and runs no process of its own, so
there is nothing to add: list one in services only to set its options. Leaving it out of
the config never deletes it, and a plan that would remove one is refused. To start over,
Delete all data empties one (see Deleting all data);
deleting the project removes everything. Auth is the part you add: it answers /api/auth
on every app address, which an app with its own sign-in uses.
A web app that sets no routes is served at a name made from its project (<domain> is
the box's domain, or its separate apps domain: see Domains):
- the project's main app at
<project>.<domain>(shop.<domain>); - every other web app at
<project>-<app>.<domain>(shop-docs.<domain>); - workers at none.
The main app is the project's only web app; else the app named like the project; else the
app named web; else the first web app in your config. Set routes to choose an address
yourself (routes: ["store"], routes: ["example.com"]). Addresses are box-wide: the
plan refuses one another project already serves.
An app keeps the address it already has: adding an app later, or upgrading a box whose
apps were served at their app names (web.<domain>, the default before addresses were
named after the project), moves nothing. For such an app the plan says so and how to keep
the old name for good (routes: ["web"]) or move it (routes: ["shop"]).
A project can be duplicated on the box, exported to a file, imported as a new project or
moved to another box (copying and moving). A stopped project (a stopped
resource, set by tiffin projects stop or a move) keeps its data but runs no apps, and
refuses deploys until it is started again.
Sharing the box
Launch as many projects as you like: they divide the box between them on their own.
Tiffin keeps memory for itself first (its services, plus Postgres's and Valkey's caches;
about 1.4 GB of a 3 GB box), and the rest is memory for apps, shared by every
project. tiffin box settings get shows the numbers.
By default a project is automatic. It grows into whatever the box has free, so a busy shop can use most of the box while the others are quiet. It can never take the platform's memory, and it always leaves 128 MB for each copy the other projects run. When the box gets tight, each project is protected up to a fair share; one that is over its share gets swapped out first, so it slows down rather than anyone being killed. CPU works the same way: full speed when the box is idle, equal shares when projects compete.
When you want a fixed share, give the project a budget in tiffin.config.ts:
export default defineConfig({
project: "guestbook",
resources: { memoryMB: 512, cpus: 0.5 }, // or: { maxSharePercent: 25 }
apps: { web: {} },
});memoryMBcaps all of the project's app copies together (production and previews) and also reserves that memory for it. All projects'memoryMBbudgets must fit in the memory for apps; the plan says so if they don't.cpuscaps its CPU, in steps of 0.25.maxSharePercentcaps it at a share of the box (memory for apps and CPUs). It is a ceiling, not a reservation, and follows the box when you move to a bigger server or resize this one (tiffin up --name <box> --type ...; the memory for apps is re-read within seconds, and Postgres and Valkey are retuned by thatup).- If both
memoryMBandmaxSharePercentare set, the lower wins.
A project at its cap is held there: an app that needs more is stopped for memory and
restarts, and the project's usage says pressure: "oom". Budget changes apply live
through plan and apply; nothing restarts. The box owner can also cap every project that
sets no budget: tiffin box settings set --default-max-share-percent 25.
A limit holds everything the project uses of the box, not only its apps. Its share is
its maxSharePercent, the box default, or what its memoryMB and cpus come to. At 25%:
- its database's queries get a quarter of the CPUs (past that they slow down; other projects' queries are not affected) and a quarter of Postgres's 100 connections;
- its KV store is held to the smaller of its
maxMemoryMBand a quarter of Valkey's memory: keys with an expiry are cleared first, then new writes are refused until it is under it (reads and deletes still work); - its builds get a quarter of the CPUs, and past its memory limit (at least 1 GB) they slow down instead of failing;
- its apps get a quarter of the weight when the disk is busy.
Every project, limited or not, has database safety limits: a query is stopped after 5
minutes, 30 seconds in a project with a limit (services.postgres.statementTimeoutSeconds changes it; one query can raise it
for itself with SET LOCAL statement_timeout), a session idle inside a transaction is
closed after 60 seconds, one query's temporary files are capped at a share of the disk,
and a project opens at most 80 connections.
When a limit holds a project back (an app restarted for memory, all its connections in use, its KV store full) it shows on the project's Usage page and in its History, at most once an hour each. Queries stopped by the time limit are only counted ("3 queries stopped today"), on Usage.
tiffin projects usage <project> shows what a project uses against its limits: memory,
headroom (how much more it could take now), CPU, disk, its database, KV store and builds
(sharePercent, database, cache, builds), each app's copies, its services and its
limitEvents.
Disk is shared too: every project's database and files live on the data disk, so the box
guards it. Past 85% full it warns (on Usage and Health), naming the project growing
fastest; past 95% that project becomes read-only (its database refuses writes, its
buckets refuse uploads, its apps' disk folders stop growing) so every other project keeps
running. An app that turns its database's read-only default off and keeps growing it
anyway is locked out of the database (its role cannot log in) until the hold lifts.
Below 90% it can write again. Each step is a change by the system in the project's
history, and undoing it lets the project write at once. Change the levels with
tiffin box settings set --disk-warn-percent 85 --disk-stop-percent 95 --disk-resume-percent 90
(a stop level of 100 only warns). For a tighter share, give a project a storage limit
(off by default; see Storage).
Changes
Every change to a project is a Change: who made it (a person or an agent, and the agent's session), why (the intent), exactly what changed, how risky it was, and the inverse needed to undo it.
The flow is always plan → review → apply:
tiffin plancomputes the steps and a plan hash. It never changes anything.tiffin apply --confirm <hash>applies that exact plan. If anything changed since you planned, it refuses and shows the new plan.
The config file is one way in, not the only one. tiffin projects manifest <project>
(GET /v1/projects/{project}/manifest) returns the project's current manifest, rebuilt
from its resources; the dashboard edits that and sends it through the same plan and
apply. tiffin pull writes it back to a readable tiffin.config.ts (it never
overwrites a file that differs without --force, and shows the diff), plus the source
of any app running a starter, so a project created in the dashboard can move to git at
any time.
Risk tiers
| Tier | Meaning | Example |
|---|---|---|
| reversible | Undo restores it | add an app, change an env var |
| outbound | Exposes data outside the box | make a bucket public |
| irreversible | Destroys data no inverse can bring back | delete Postgres, delete a bucket |
Every step says, in plain words, why it has its tier.
Undo
tiffin undo <change> applies the change's inverse, after showing you the plan.
It refuses if something the change touched was modified since, so it never silently
overwrites newer work. Deleting a database or bucket keeps a snapshot or trash copy
for seven days.
People and API keys
- People use the dashboard with a role: owner, admin, member or viewer. They sign in with a one-time link or a passkey.
- Agents, scripts and CI use API keys. A key reaches some projects (
all, which includes projects created later, or a list) with full access (read, plan and apply any change, deleting data included) or read access (read and plan only). It can expire after 30 or 90 days, or never. - A key with full access to all projects is the box admin: it also manages keys, people and exports. No other key can manage keys.
- Outside its reach a key gets
403 forbiddenwith a plain reason ("this key is read only", "this key can only reach shop"). There is no approval step: the agent's own client asks you before destructive tools, and History records everything.
Something wrong or unclear? Edit it on GitHub.