> ## Documentation Index
> Fetch the complete documentation index at: https://tesser.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Put a repo on Tesser

> The guide your agent follows to put a repo on Tesser for the first time, exactly as `tesser help setup` prints it

Your agent reads this page with `tesser help setup` when it puts a repo on Tesser, so it is written to the agent. What it leaves behind is the repo's own skill at `.agents/skills/tesser/`, which every agent after it reads instead.

`tesser --help` and `tesser service --help` are the reference for the tool and
the manifest format. Read both first and trust them over this guide.

You are building for the *next* agents. Each arrives in a fresh worktree with
a task and no human to ask, and reads only the repo's tesser skill, never
this guide or tesser's. Most of their tasks need one of three things, cheapest
first, and the repo skill gives each as one command:

1. **A focused check**: a test file or a typecheck, on the worktree's
   workbench with nothing running (`tesser exec -- <cmd>`).
2. **The running app, no code change**: open a page, sign in, create a
   record. This needs **zero personal boxes**: it runs against the org's
   shared instances, which you pin.
3. **The running app with a change**: `tesser dev` only the services the
   change touches; every other dep stays on its shared instance.

A cold personal box takes minutes to tens of minutes; a shared instance is
already up. What makes path 2 instant is shared instances of the whole app,
built once by you. What makes paths 2 and 3 usable is a sign-in an agent gets
in one command.

## Order of work

Most of setup is waiting on cold boxes, installs and migrations. Never wait
on one thing while something else could run:

* **Anything that can take over 30 s runs in the background** (your tool's
  background option): `tesser dev`, `make`, `snapshot create`, migrations,
  installs, test runs, the focused check you time for the repo skill. Its
  exit arrives as a notification on a later tool call. Never write a
  foreground loop that sleeps on a file, log or task output.
* **Keep a queue of work that does not need a running box**, and work it while
  things boot: the other manifests, run scripts, `login.sh`, `create.sh`,
  `db.sh`, `up.sh`, the repo skill, reading the auth code.
* **Block only when the queue is empty**, on the one thing you need next:
  `tesser wait <box>` in the foreground, at most 9 minutes per call. A
  minute after each launch, read `tesser logs <box>`: a box that crash-loops
  on a bad env value or a missing package shows it there long before `wait`
  gives up. Never end your turn to wait: background tasks and their
  notifications die with it.

1. In the first few minutes, write every manifest you can from the repo's
   own config and start them all at once, in the background. Infra holds
   every database, queue, cache and search container the app's migrations
   or boot touch (read the compose files and the settings for each), because
   adding one later re-pins infra onto an empty disk. After that, rerun the
   migrate step from an app box (`tesser exec <box> -- bash
   .agents/skills/tesser/app-run.sh migrate`): pinning the app again at the
   same sha does nothing while it is healthy. Start infra with
   `tesser make <infra> --ensure-running <full sha>`, each app box with its
   first cold `tesser dev`. Fix a manifest and restart only that box.
2. While they boot, write the dev user, `login.sh` and `create.sh` against
   the code, the repo skill, and `up.sh`. Once infra and your app box are up,
   migrate and seed once from it in the background (section 1, One service
   migrates).
   Test each script the moment its box is up.
3. When `login.sh` and `create.sh` work against your own app box, the box
   config is final: pin every app service the main page needs, frontend
   included, at your HEAD (start from main, so HEAD's migrations are
   main's), and `tesser snapshot create --sha <HEAD>`, all in
   the background, at once. Pinning from the worktree whose own box of that
   service works hands that warm box over as the shared instance; your next
   `tesser dev` of it starts a new box. Pin each once: a re-pin, or a pin
   with no box of yours to hand over, is a new box, provisioned cold.
4. As soon as your own box passes the browser check, tell the user the URL
   and how to sign in. Then prove path 2 from a fresh worktree (section 5).

## 1. Up

Start from the repo's own dev and agent tooling (README, `docker-compose*`,
`Makefile`, `package.json` scripts, `.env.example`, `.env.agent`,
`.devcontainer`, devservices, CI) and override only hosts, ports and URLs.

* One `.toml` per process in the repo's tesser skill directory, the first
  that fits:

  1. `.agents/skills/tesser/`, when the repo has `.agents/skills` or neither.
     Link `.claude/skills -> ../.agents/skills`, or only
     `.claude/skills/tesser -> ../../.agents/skills/tesser` when
     `.claude/skills` already holds other skills.
  2. `.claude/skills/tesser/`, when the repo keeps its skills only in
     `.claude/skills`. Link `.agents/skills/tesser ->
     ../../.claude/skills/tesser`.

  A link is not a second manifest directory. Paths below say
  `.agents/skills/tesser`; use yours. Templates under Templates below.
* Pin every port. `[run] dev` points at a run script that `exec`s the process.
  `[health]` with a real `path` and a timeout sized for the cold first install
  and compile. A port answering is not health.
* **Lean setup.** Install only what the dev processes import: in a monorepo,
  only those workspaces (`pnpm --filter=<app>... install`, `yarn workspaces
  focus`). Runtimes come prebuilt into `$HOME` (the image's fnm, `uv`, `mise`
  with `ruby.compile=false`); a setup that compiles a runtime from source is
  a bug. Skip native builds the dev server never loads. Never invent a secret
  the code validates: copy it from the repo's example env. Time each step;
  cut any over 2 minutes the page does not need.
* **Declare what setup reads** in `[run] setup_inputs`: lockfiles, every
  package.json (`**/package.json`: globs match from the repo root), patches,
  `.npmrc`, the setup script itself, and the sources of anything setup builds.
  tesser skips setup when none changed since it last succeeded on that disk,
  a snapshot's disk included; `tesser dev --rerun-setup` runs it anyway. A
  skip skips all of setup, so a build input left out means a stale build.
  Inside setup, gate each install or build on a hash of its files' contents
  (`git ls-files -z -- <files> | xargs -0 sha1sum | sha1sum`, stamp in
  `~/.cache`), never git's index or mtimes, so a rerun redoes only what
  changed.
* **Slow state is shared.** Databases, queues and search run in one org
  shared instance, pinned first, never in a container on an app box, whose
  disk a re-pin replaces with a new box. If the repo's provisioning puts its
  databases on the app machine (an all-in-one dev VM, `tools/provision`,
  `bin/setup` with system services), do not split it: pin that whole
  service. Shared instances that share state (one redis, one set of queues)
  are pinned at the same sha.
* **One service migrates.** For each database, the app service that owns its
  schema (usually the API) migrates and seeds it from its run script when
  `$TESSER_SLOT` is `pinned`, which only shared instances get (template
  below). The step runs on every start of the shared instance, and a run
  script that exits takes the instance down until someone re-pins it. So it
  waits for the database, uses the deploy form of migrate (`prisma migrate
  deploy`, `drizzle-kit migrate`, `rails db:migrate`, `alembic upgrade
  head`, `manage.py migrate --noinput`), never `migrate dev`, `db push`,
  `db:reset`, `db:setup` or anything that prompts, and seeds only when a row
  only the seed makes is missing. Before the app is pinned, run it once from
  your own app box with the run script's env (`tesser exec <box> -- bash
  .agents/skills/tesser/app-run.sh migrate`) so `login.sh` has data; the
  pin's run then finds nothing to do.
* **Nobody else touches the shared schema**: not workers or frontends, not
  task agents, not `[run] setup` (it also runs on every personal box and on
  the snapshot builder). Pin only commits whose migrations are on main; a
  branch with its own migrations uses its own database (below).
* **Worktrees stay apart inside shared state.** Every worktree signs in to its
  own tenant (section 2). Redis key prefixes, queue names and bucket
  prefixes carry `$TESSER_SLOT`. A worktree whose change adds migrations gets
  its own database copied from the shared one (`pg_dump | psql` into a new
  database; `CREATE DATABASE … TEMPLATE` refuses while the shared app holds
  connections), names it in its `.env.tesser`, and applies only its new
  migrations there (`app-run.sh migrate`); never a migrate from empty, and
  never against the shared one.
* **What survives.** Snapshots and pool boxes carry `/home/ubuntu` and
  docker's data, not `/usr`, `/opt` or apt packages. System packages go in the
  manifest's `apt = [...]`: tesser caches their .debs in `$HOME` and installs
  them before setup on every start, offline on a box from a snapshot. Give
  every manifest the same `apt` list: a service whose list differs reruns
  setup on boxes from a snapshot. Setup writes only under `$HOME` and docker.
  A write under `/etc`, `/usr`, `/opt` or `/var/lib`, a process left running,
  or a container without `restart: unless-stopped` is logged and makes setup
  rerun after every reboot and on every box from a snapshot: a warm-up stops
  everything it starts, and infra setup only pulls images.
  `setup` also runs on the snapshot builder, which has no box id or address:
  anything per box goes in the run script. A database can outlive the files it
  points at: when the app supports object storage, uploads go to object
  storage (the user's test bucket, else minio on the database's shared
  instance), never the app box's disk, which a re-pin replaces with a new
  box.
* **Outside services** (email, OAuth, payments, LLMs, object storage): use the
  user's own keys. Copy them from their local env file (`.env`, `.env.local`)
  into a gitignored file you `tesser env push` to your boxes. Shared
  instances only get values through `tesser env set`, which is the human's
  call: ask, or use stand-ins there. Test-mode keys only (Stripe `sk_test_`,
  a sandbox account): a live or production key becomes a stand-in too. Where
  there is no key: a local stand-in (mailpit, minio), a fake value where the
  app only checks one is set, or the app's switch to turn the feature off.
  Never commit a key.
* **`up.sh`**: path 2 by default. It checks every shared instance exists and
  prints the shared app's URL; a missing one is an error naming the service,
  never something it pins. `up.sh <svc>...` is path 3: it `dev`s those
  services from this worktree and prints their URLs. It never wires or
  restarts a shared instance.

Facts about boxes that are easy to get wrong:

* **tesser syncs only what git does not ignore.** Run
  `git status --ignored .agents .claude` after writing files.
* **Node** comes from the image's fnm, at the repo's pinned version. Use the
  block under Templates below; never install node or overwrite
  `/usr/local/bin` (pnpm and yarn there are corepack shims). If the repo
  installs its own node system-wide, remove those shims first, in the run
  script. A node version the image lacks installs under `/usr/local/fnm`, so
  setup reruns after a reboot until the image has it.
* **Host and origin.** The box receives every request as `Host: localhost:P`,
  with `Origin`/`Referer` rewritten to `http://localhost:P`: base URLs,
  `ALLOWED_HOSTS`, CSRF trusted origins, auth callback URLs and any absolute
  URL the server hands the browser name `localhost`. The laptop sends a
  page's calls to `localhost:<port>` to that page's own box. A URL with no
  page behind it (an email link, a redirect back from an outside site) names
  the box instead: `http://$TESSER_BOX_HOST:P`.
* **A page that calls another port** reaches its own box's port, but cookies
  set on the page's host do not follow. Proxy the API through the frontend dev
  server (Vite `server.proxy`, Next rewrites) when auth is a cookie.
* **Compile-time public env** (`NEXT_PUBLIC_*`, `VITE_*`) must be in the run
  script's environment when the dev server starts.
* **CSP and dev-origin checks** must accept `*.localhost`.
* **`tesser ssh` runs on the box as it is**, with no sync first; `tesser
  exec` and `tesser sync` copy the worktree.
* **A run script edit** may not restart the dev server: after one,
  `tesser sync <box> --restart`.

## 2. Signed in

Find how the app makes a session (the login route, the cookie or token name,
where the frontend keeps it: cookie, `localStorage`, both) by reading its auth
code, then build:

* **A user and tenant per caller.** `login.sh [name]` signs in as
  `dev+<name>@example.com`, creating that user idempotently with a constant
  password, its own tenant (workspace, org, team) named after it, an admin
  role, verified email and onboarding done. `name` defaults to the worktree's
  folder. Use the app's own seed, CLI, management command or signup API;
  SQL last. Two worktrees never share a user, so they never revoke each
  other's sessions or see each other's records.
* **Box-only switches** that remove the human: email verification off or
  auto-confirmed, captcha off, the first-run wizard pre-completed. Gate each
  on an env var only boxes set. Every request reaches the box from one
  address, so while reading the auth code find its per-IP rate limits. Where
  the app takes the client IP from the leftmost `X-Forwarded-For` entry,
  `login.sh` and `create.sh` send an IP made from a hash of the caller's
  name (`10.<a>.<b>.<c>`); the laptop daemon keeps it and appends its own
  hop. Check with curl that two names get separate limits. Otherwise, or if
  they don't, skip the limit where `$TESSER_SLOT` is set, before you pin the
  app service (Order of work step 3): adding it later restarts the shared
  instance. Where the app
  only offers magic links or OAuth, enable its password or dev-login path, or
  catch the email in mailpit and script the click.
* **Every `login.sh` call makes an independent session** (vary the device id
  or User-Agent if the app ties sessions together), with the app's real login
  request, never a forged token. It prints JSON a browser can load
  (`{"cookies":[{"name","value"}],"localStorage":{...}}`) on stdout and
  writes a curl jar inside the worktree (gitignored), so two worktrees never
  share one. Prove independence: mint A, mint B, load B
  in a browser, then A must still be signed in. curl sends no `Origin`, so
  a cookie POST can be refused as cross-site (a 403 such as
  `CSRF_ORIGIN_MISMATCH`): send `-H 'Origin: http://localhost:<port>'`, or
  use the app's API token path. A jar is good only for the host it was
  minted on: a script that can target either the shared box or the
  worktree's own box puts the host in the jar's file name. The session you hand anyone
  comes from a `login.sh` call after your last check and nobody has opened:
  some apps rotate a refresh token on use and revoke the session when the old
  one comes back. Find out: load one session in two fresh browsers one after
  the other; if the second is signed out, say so in the repo skill.

Prove sign-in with curl against the auth endpoint first, printing status and
body; the real error shows there in a second. Then load a signed-in page in a
headless browser with only what `login.sh` printed. Browser scripts: never
automate the login form; wait for `domcontentloaded` and one known selector,
at most 20 s each, never `networkidle` against a dev server. The one exception:
a dev server that compiles per route (Next, Vite) can take 60-90 s on a route's first load:
give that load 90 s once, never a retry loop, and put the measured first and
warm load in the repo skill. On failure, or
when the page lands on a login, setup or onboarding URL, print the final URL,
the first 800 characters of text, and every response >= 400. A signed-in
page is done when it loads with no page errors and no failed request to the
app's own hosts: fix each one, or name it in the repo skill with why it is
harmless.

## 3. Data

* **A tenant worth testing in.** The first `login.sh <name>` also fills that
  caller's tenant with what a real customer has after onboarding: the app's
  top-level object and the things its main pages show, made through the
  app's API (a workspace with a project and some issues; an org with a
  deployment and a few pages; a team with a published survey and responses).
  A task agent then exercises real flows on its first call instead of
  building data. Later calls only sign in.
* **`create.sh <name>`**: one more of the app's main records in the caller's
  tenant, printing the URL of the page where its name shows verbatim. Note
  name rules (characters, case, length; a slug is not a name).
* **`db.sh`**: a database shell from the laptop (`tesser ssh <infra> -- docker
  exec -i <db> psql …`) and `db.sh -c '<sql>'` for one query. Say which
  tables hold the main records.
* Those plus `up.sh` are all the next agent uses: prove them first.

## 4. The repo's tesser skill

Template under The repo's own skill, below. In this order: focused checks, Up
(paths 2 and 3, with how to wait and how long each took you), Sign in,
Data, Iterate, When it breaks. Copyable commands.
Explain why in a sentence where the next agent would otherwise guess.

Shared instances belong to whoever sets the repo up. The repo skill tells
task agents to read from them and write only in their own tenant, and to
report a missing or broken one (service, box, error) rather than rebuild it.

## 5. Prove it from zero

Commit, `git worktree add` a fresh checkout, and run exactly what the next
agent would for path 2, in your own turn and in the foreground, reading only
the repo skill: `up.sh`, `login.sh`, `create.sh PROOF<6 random A-Z0-9>`, then
load the printed URL with the `login.sh` session in a headless browser and
require the marker, case-sensitively, in the page's text. Every place it is
slow or wrong is a fix to a script or the skill: fix it and re-run only the
step that failed.

Start path 3 for the main app service from that worktree in the background
before the proof and note its cold time if it lands before you finish; never
wait on it, and write "not measured" in the repo skill if it has not.

Last, `tesser feedback "<what you ran, what happened, what you expected>"`
for the thing in tesser itself, not the repo, that cost you the most time.

# The repo's own skill: `.agents/skills/tesser/SKILL.md`

This is the only file the next agent reads. Cut any section the repo does not
need; never pad one.

```markdown theme={"theme":"css-variables"}
---
name: tesser
description: Run <repo>'s tests and app on tesser boxes, sign in as your own dev user, and read or write its database. Use for any test, typecheck, running page, signed-in check or test data in <repo>, or when a <prefix>-* box misbehaves.
---

# tesser

For tesser itself, `tesser --help`. This file is only what is specific to this
stack. Pick the cheapest path that proves your task.

| service | manifest | port | shared instance | what it is |
| ------- | -------- | ---- | --------------- | ---------- |

## Waiting
Run every command below in the foreground with your tool's timeout raised to
10 minutes (`timeout: 600000`). If one times out, `tesser wait <box>`; never
background it and poll.

## Focused checks
`tesser exec -- <the test command> <file>`, `tesser exec -- <typecheck>` on
the worktree's workbench with nothing running. First run installs
(<measured>); <what needs a build first, and how>.

## Up
- **No code change** (open a page, sign in, create data):
  `bash .agents/skills/tesser/scripts/up.sh` makes no boxes and prints the
  shared app's URL, <measured>.
- **With a change to <svc>**: `bash .agents/skills/tesser/scripts/up.sh <svc>`
  runs your worktree's <svc> against the shared rest and prints its URL:
  <measured> cold, <measured> warm.

## Sign in
`bash .agents/skills/tesser/scripts/login.sh` signs in as your own user,
`dev+<worktree>@example.com`, admin of its own <tenant>, created on first
call. It prints `{"cookies":[...],"localStorage":{...}}` for a browser and
writes a curl jar to `.tesser/<app>-<host>.jar` in this worktree. Each call is a new session. Hand on
a fresh call's output, made after your last check and never opened by it.
<Rotation or expiry rules if any.>

## Data
- Your tenant starts with <what login.sh made: the top-level object and its
  contents, with names and URLs>.
- `bash .agents/skills/tesser/scripts/create.sh '<name>'` makes a <record>
  in your tenant and prints the URL where its name shows. <Name rules.>
- `scripts/db.sh -c 'select …'`; main records live in <tables>.
- Write only in your own tenant. Never seed, reset, migrate or change flags on
  a shared instance: its run script migrates and seeds it. A change with
  migrations gets its own database (Iterate).

## Iterate
<what hot-reloads, what needs `tesser sync <box> --restart`, what needs a
migration and how a worktree gets its own database for one, named in its
`.env.tesser` before it runs `app-run.sh migrate`.>

## When it breaks
Save the command, box id and full error. Check one layer at a time: the box
(`tesser status <box>`), its server (`tesser logs <box>`), its deps (bare
`tesser wire <box>`), then your own data. A shared instance that is missing or
unhealthy is not yours to fix: report the service, box and error. After one
failed recovery, report the blocker. When tesser itself misled you (an
error, a missing command, a workaround), `tesser feedback "<what happened>"`
and keep going.
- `<exact error string>`: <cause>. <fix>. (Only errors an agent will hit
  again; at most five.)

## Hand-off
Worktree and sha, each box and its role, wires, your tenant, the command, its
log and exit code. Remove the boxes you made when the task is done.
```

# Templates

Generic versions of a working frontend + backend setup. Replace `app`,
ports and commands; keep the shape. Paths assume `.agents/skills/tesser/`.
Check field names against `tesser service --help` before committing.

## Manifest: an app server

```toml theme={"theme":"css-variables"}
# <one line: what this is and where its env comes from>
ports = [5000]
size = "large"

[run]
setup = "bash .agents/skills/tesser/app-up.sh"
dev = "bash .agents/skills/tesser/app-run.sh"
setup_inputs = ["**/package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", ".npmrc", "patches/**", "src/**", "tsconfig.json", ".agents/skills/tesser/app-up.sh"]

[health]
path = "/healthz"
timeout = "900s"

[deps]
5432 = "app-infra:5432"
6379 = "app-infra:6379"
9000 = "app-infra:9000"   # minio

[env]
required = ["APP_NPM_READ_TOKEN", "SESSION_SECRET"]
```

## Manifest: infra containers

```toml theme={"theme":"css-variables"}
# Containers only. Every port is docker-published, which reads as healthy
# even though no host process owns it.
ports = [5432, 6379, 9000]
size = "large"

[run]
setup = "bash .agents/skills/tesser/infra-up.sh"
dev = "bash .agents/skills/tesser/infra-dev.sh"
setup_inputs = [".agents/skills/tesser/compose.tesser.yml", ".agents/skills/tesser/infra-up.sh"]

[health]
timeout = "600s"
```

`infra-dev.sh` runs `docker compose -f .agents/skills/tesser/compose.tesser.yml up`
in the foreground, then a provision step (buckets, queues) on every start,
because in-memory emulators keep nothing across restarts. Pin image tags: a
floating tag that stops pulling takes every new infra box down with it.

## Setup: `app-up.sh`

```bash theme={"theme":"css-variables"}
#!/usr/bin/env bash
# Setup for app boxes. tesser skips it when nothing it hashes changed; every
# snapshot build runs it.
set -euo pipefail
cd "$(dirname "$0")/../../.."

# The image's fnm reads .nvmrc / .node-version, partial versions included; pnpm
# and yarn are already corepack shims. Don't overwrite /usr/local/bin.
export FNM_DIR=/usr/local/fnm PATH=/usr/local/fnm:$PATH
eval "$(fnm env --shell bash)"
fnm use --install-if-missing

[ -n "${APP_NPM_READ_TOKEN:-}" ] ||
  { echo "APP_NPM_READ_TOKEN is unset; tesser env set app APP_NPM_READ_TOKEN" >&2; exit 1; }
npm config set //registry.npmjs.org/:_authToken "$APP_NPM_READ_TOKEN" >/dev/null

# Install and build only when a tracked input changed. The hash covers the
# files' contents (never git's index, mtimes, the stamp or the output) and the
# stamp lives outside the tree.
stamp() { mkdir -p ~/.cache/tesser-stamps; echo ~/.cache/tesser-stamps/"$1"; }
inputs() { git ls-files -z -- "$@" | xargs -0 sha1sum | sha1sum | cut -c1-40; }

h=$(inputs package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc patches)
if [ "$(cat "$(stamp install)" 2>/dev/null)" = "$h" ] && [ -d node_modules ]; then
  echo "deps up to date; skipping install"
else
  pnpm install --frozen-lockfile --filter=app...
  echo "$h" > "$(stamp install)"
fi

h=$(inputs src package.json tsconfig.json)
if [ "$(cat "$(stamp build)" 2>/dev/null)" = "$h" ] && [ -f dist/server.js ]; then
  echo "build up to date; skipping"
else
  pnpm build
  echo "$h" > "$(stamp build)"
fi

# Warm-up only while a snapshot is being built, once per snapshot.
WARM_STAMP="$HOME/.tesser/app-warm-$(git rev-parse HEAD)"
if [ -f "$HOME/.tesser/snapshot-build" ] && [ ! -f "$WARM_STAMP" ]; then
  bash .agents/skills/tesser/app-warm.sh && touch "$WARM_STAMP"
fi
echo "app ready ($(node -v))"
```

## Run: `app-run.sh`

```bash theme={"theme":"css-variables"}
#!/usr/bin/env bash
# Starts the app with the box env. Last wins: committed example, overrides,
# then a gitignored .env.tesser pushed with `tesser env push`.
set -euo pipefail
cd "$(dirname "$0")/../../.."
: "${TESSER_SLOT:?tesser did not set it; update the tesser CLI}"

# A dotenv file is not shell: read its KEY=VALUE lines, never source it.
loadenv() {
  local line k v
  while IFS= read -r line || [ -n "$line" ]; do
    line=${line%$'\r'}
    [[ $line =~ ^[[:space:]]*(export[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=[[:space:]]*(.*)$ ]] || continue
    k=${BASH_REMATCH[2]} v=${BASH_REMATCH[3]}
    if [[ $v =~ ^\"(.*)\"[[:space:]]*(#.*)?$ || $v =~ ^\'(.*)\'[[:space:]]*(#.*)?$ ]]; then v=${BASH_REMATCH[1]}
    else v=${v%%[[:space:]]#*}; v=${v%"${v##*[![:space:]]}"}; fi
    export "$k=$v"
  done < "$1"
}
loadenv ./.env.example
set -a
APP_ENV=local
PORT=5000
# The box sees itself as localhost: the daemon sends Host: localhost:5000.
PUBLIC_API_URL="http://localhost:5000"
# Per-worktree state on shared infra.
QUEUE_NAME="jobs-$TESSER_SLOT"
REDIS_URL="redis://127.0.0.1:6379/0"
REDIS_KEY_PREFIX="$TESSER_SLOT:"
set +a
# Your own keys stay off a shared instance, even one handed over from your box.
[ "$TESSER_SLOT" = pinned ] || [ ! -f ./.env.tesser ] || loadenv ./.env.tesser

export FNM_DIR=/usr/local/fnm PATH=/usr/local/fnm:$PATH
eval "$(fnm env --shell bash)"
fnm use --install-if-missing

# Only the service that owns the schema. Runs on every start of the shared
# instance, so it waits for the database and seeds only an empty one.
migrate() {
  for i in $(seq 60); do pnpm db:migrate:deploy && break; [ "$i" -lt 60 ] || exit 1; sleep 5; done
  pnpm -s db:seeded || pnpm db:seed   # db:seeded exits 0 iff a row only the seed makes exists
}
[ "${1:-}" != migrate ] || { migrate; exit; }
[ "$TESSER_SLOT" != pinned ] || migrate
exec node dist/server.js
```

## Scheduled refresh of the shared backend

```yaml theme={"theme":"css-variables"}
name: refresh tesser stack
on:
  workflow_dispatch:
    inputs:
      ref: { description: a commit on main to pin, required: false, default: main }
  schedule:
    - cron: "0 13,16,19,22 * * 1-5"
concurrency: { group: tesser-stack, cancel-in-progress: false }
permissions: { contents: read, id-token: write }
env:
  TESSER_ORG: org_xxx
  TESSER_GRANT: grt_xxx    # a grant for this repo's main branch, carrying the AWS role
jobs:
  refresh:
    runs-on: ubuntu-latest
    timeout-minutes: 90
    steps:
      - uses: actions/checkout@v5
        with: { fetch-depth: 0, ref: "${{ inputs.ref || 'main' }}" }
      - run: curl -fsSL https://tesser.sh/install | sh
      - run: tesser login oidc --org "$TESSER_ORG" --grant "$TESSER_GRANT"
      - name: pin the shared services at one sha
        run: |
          set -euo pipefail
          SHA="$(git rev-parse HEAD)"
          for s in app-server app-worker; do   # the schema owner first tesser make "$s" --ensure-running "$SHA"; done
      - name: snapshot the repo at main
        if: ${{ (inputs.ref || 'main') == 'main' }}
        run: |
          set -euo pipefail
          SHA="$(git rev-parse HEAD)"
          if tesser snapshot ls --json | jq -e --arg sha "$SHA" \
              'any(.[]; .current and (.stale | not) and .sha == $sha)' >/dev/null; then
            echo "current snapshot is already at $SHA"
          else
            tesser snapshot create --sha "$SHA" --wait
          fi
      - if: always()
        run: tesser service
```

Infra is not in the loop: a new infra shared instance is a new, empty database.
Re-pin infra only by hand, and right after it rerun the migrate step
(section 1, One service migrates).
Match the repo's other workflows: if they pin action versions or tool
checksums, pin tesser the same way ([https://tesser.sh/docs/install](https://tesser.sh/docs/install.md) lists each
release's sha256).

## `up.sh`

```bash theme={"theme":"css-variables"}
#!/usr/bin/env bash
# up.sh            no code change: shared instances only, prints the app URL
# up.sh web api    dev those services from this worktree against the shared rest
set -euo pipefail
cd "$(dirname "$0")/../../../.."
SHARED=(app-infra app-api app-web)
for s in "${SHARED[@]}"; do
  tesser service ls --json | jq -e --arg s "$s" 'any(.[]; .service == $s and .defaultBoxId != null)' >/dev/null ||
    { echo "no shared $s; ask whoever owns this repo's tesser setup to pin it" >&2; exit 1; }
done
if [ $# -eq 0 ]; then
  echo "http://$(tesser service ls --json | jq -r '.[] | select(.service=="app-web") | .defaultBoxId | sub("_";"-")').localhost:3000"
  exit
fi
for s in "$@"; do
  box=$(tesser dev "$s")
  echo "$s: http://${box/_/-}.localhost:$(sed -n 's/^ports = \[\([0-9]*\).*/\1/p' ".agents/skills/tesser/$s.toml")"
done
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.