Documentation Index
Fetch the complete documentation index at: https://recipe.uselettuce.dev/llms.txt
Use this file to discover all available pages before exploring further.
Self-hosted Lettuce — install guide
This page is the runbook a Lettuce operator follows to stand up their own instance. It is opinionated: when there is a sane default, we pick it. When the v1 product is missing a piece, we call that out explicitly rather than pretend it’s there. If you’re evaluating self-hosted as a concept, start with RFC-001 for the design decisions and RFC-002 for how the binaries are built. This page assumes you’ve already decided to install.Status: v0 install path. The Docker Compose flow is fully working end
to end; the Helm chart is a skeleton (see §4). If you hit something this
guide doesn’t cover, write to founders@uselettuce.dev — we’ll help
you finish the install and feed the gap back into this page.
0. Quick start with lettuce-init
If you’d rather not hand-edit YAML, the lettuce-init CLI (LET-31)
turns a license key into a working install layout in one command:
.envpre-wired with your license, your domain, the rightLETTUCE_TELEMETRY_ENABLED=true(LET-27 caught the naming: it is*_ENABLED, not*_DISABLED), and freshly-minted 32-byte URL-safe random secrets forPOSTGRES_PASSWORD,CODEWAZE_ADMIN_TOKEN,JWT_SIGNING_KEY, andMINIO_ROOT_PASSWORD. The CLI refuses to overwrite the.envwithout--forceso a re-run doesn’t silently rotate secrets out from under a running install.docker-compose.ymlcopied from this repo with image tags pinned to your--version.public.pem(the bundled dev key by default — swap for your per-customer production key beforedocker compose up).README.mdwith the exact next-step commands, including the real first-admin flow (POST /api/admin/invites).
Honest caveat — image pulls don’t work yet. LET-17 wired the
GitHub Actions pipeline that will publish per-service images to
ghcr.io/uselettuce/<svc> on tag push, but the uselettuce GitHub
org doesn’t exist publicly yet. Until then docker compose pull
will fail with unauthorized or manifest not found. The
generated README explains the workaround (private registry token
or local builds). Watch this space — once the org is up the same
generated layout will pull cleanly without changes.1. Prerequisites
Host
Pick one of:- Single Linux host with Docker Compose. Docker Engine 24+ and the
docker composeplugin. Suitable for a pilot, a single-team install, or a non-production demo. - Kubernetes cluster. 1.27+ with Helm 3.12+. Suitable for production.
Sizing (per service node)
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 4 vCPU | 8 vCPU |
| RAM | 8 GB | 16 GB |
| Storage | 100 GB block storage | 250 GB SSD |
Data plane
- Postgres 15+ if you’re bringing your own. The Compose stack ships a
Postgres 16 container; the Helm chart expects a
DATABASE_URLyou point at RDS / Cloud SQL / Supabase / a bare instance. - No Redis required. Background jobs run on Postgres
LISTEN/NOTIFY(seesrc/codewaze/service/jobs.py), so the worker and API share one database and that’s it. - Object storage — S3-compatible. The Compose stack ships MinIO; the
Helm chart expects you to point
CODEWAZE_S3_*at AWS S3, Cloudflare R2, Backblaze B2, or your own MinIO/Ceph deployment.
Network
Lettuce needs a reachable HTTPS hostname. OAuth + GitHub-App callback URLs come back over HTTPS, so plaintext-only deployments aren’t supported. Terminate TLS in front of theweb service with whichever
ingress you already use:
- Caddy / Traefik / nginx-ingress with Let’s Encrypt is the cheap default.
cert-manager+ ClusterIssuer on Kubernetes is the obvious choice if you already run it.
GitHub integration
You need permission to register a GitHub App on the GitHub instance Lettuce will index — either:- github.com (free / Pro / Team / Enterprise Cloud), or
- a GitHub Enterprise Server instance you operate (LET-20 — see §5).
GITLAB_HOST (LET-21 — see §5).
Bitbucket Data Center is still on the roadmap (LET-15.6) — not yet
in v1.
License key
Self-hosted Lettuce is license-gated. The worker refuses to boot without one whenLETTUCE_SELF_HOSTED=1. Get yours by writing to
sales@uselettuce.dev or via the contact form on
uselettuce.dev/self-hosted. The
team will issue a signed Ed25519 JWT with your seat/repo cap and
expiry baked in. Expect a turnaround of one business day.
Pricing tiers are not yet published. Per RFC-001 §7, the founder
hasn’t finalised whether the floor is Team-5 or Team-25, or whether
billing is per-seat / per-LOC / hybrid. For pilots today: contact sales,
and we’ll quote against your team size + repo count.
2. License & telemetry — read this first
The license and telemetry models cause more confusion than any single piece of YAML, so we cover them up front.What the license does
WhenLETTUCE_SELF_HOSTED=1, the worker and API call gate_boot_or_raise
on startup (src/codewaze/service/license.py). That:
- Reads
LETTUCE_LICENSE_KEY(a JWT) andLETTUCE_LICENSE_PUBLIC_KEY(the Lettuce Ed25519 public key — bundled in the runtime image at/var/lib/lettuce/public.pem, default works). - Verifies the JWT signature against the public key.
- Verifies the JWT isn’t past its
expclaim. - Writes a cache of the last-good claims to
/var/cache/lettuce/license.json(override withLETTUCE_LICENSE_CACHE_PATH).
src/codewaze/service/entitlements.py) when
billing-gated features run. Hitting a cap surfaces in the UI as a
“License limit reached” badge, not a crash.
What the telemetry receiver sees
Self-hosted instances POST a daily heartbeat tohttps://telemetry.uselettuce.dev/v1/self-hosted/telemetry. The payload
is exactly these fields:
src/codewaze/service/telemetry.py
(parse_ping) for the schema, and tests/test_self_hosted_telemetry.py
for the contract tests that pin it.
Telemetry is on by default unless your license’s
telemetry_required claim is false — that’s the regulated-customer
escape hatch. The Compose stack and the Helm chart both expose:
| Env var | Default | Meaning |
|---|---|---|
LETTUCE_TELEMETRY_ENABLED | true | Operator-side switch. Set to false (also accepts 0, no, off) to suppress emission.1 |
LETTUCE_TELEMETRY_URL | https://telemetry.uselettuce.dev/v1/heartbeat | Override the receiver (rarely useful unless you proxy it). |
Operator opt-out is only honoured when the license allows it. If your
license was issued with
telemetry_required=true and you flip
LETTUCE_TELEMETRY_ENABLED=false, the worker logs the contradiction and
keeps pinging. Customers who need an offline-only key should request one
explicitly from sales — see RFC-001 §3.Sender path is shipping incrementally. LET-30 landed the receiver
endpoint + ops dashboard. The instance-side daily sender lands in
LET-30.1 — until then
LETTUCE_TELEMETRY_ENABLED is wired through env
but the daily POST is gated by that follow-up.3. Install — Docker Compose
This is the easy path. One host, onedocker compose up, ~3 minutes from
clone to a working stack.
Repo URL is a placeholder. Until we cut the public install artifact
(RFC-001 §10 —
lettuce/install repo + signed tarballs), clone from the
internal mirror your sales contact shares with you. The path the rest of
this guide assumes is coze/self-hosted/.Write the .env file
/var/lib/lettuce/public.pem, so the default works out of the box.
Override LETTUCE_LICENSE_PUBLIC_KEY only if you’re testing with a
custom keypair (see self-hosted/README.md).For GitHub integration, also fill the four GITHUB_APP_* vars. If
you’re targeting GitHub Enterprise Server, set GITHUB_HOST to your
GHES hostname (and NEXT_PUBLIC_GITHUB_HOST for the web bundle —
see §5).Set LETTUCE_SELF_HOSTED
The bundled
docker-compose.yml already sets LETTUCE_SELF_HOSTED=true
on api and worker. Don’t unset it — entitlements + license gating
are wired off this single flag.Start the stack
ghcr.io/uselettuce/{api,worker,web}:${LETTUCE_VERSION}
(LET-17). First boot takes ~30s while Postgres comes up and the
idempotent schema applies.Verify health
docker compose logs worker will
show one of the LicenseError subclasses (see §11).Create the first user
The first-admin workflow runs through the API, not a dedicated
The first signup creates an account; subsequent signups against the
same invite code join it as teammates. Generate per-user invites
afterwards from the Team page in the dashboard.
lettuce admin create command (no such command exists today — see
the caveat in the closing notes). Mint an invite with your admin
token, then redeem it through the web signup form:4. Install — Kubernetes (Helm)
my-values.yaml:
The Helm chart is a v0 skeleton. Read this before you push it at
prod. Tracked under LET-15.1 / LET-15.x:
- No Ingress template. You have to bring your own Ingress (or
Gateway API resource) and point it at the
webandapiServices. TLS termination is on you —cert-manager+ a ClusterIssuer is the obvious answer. - No PVCs. The
webandapiDeployments don’t mount persistent storage. Postgres is expected externally; MinIO isn’t templated. Setpostgres.external.url+s3.external.*to real-managed services. - No HPAs. Single replica per service by default. Scale manually
via
helm upgrade --set api.replicas=N. - No probes. Liveness/readiness probes are not wired. Pods come up
on container start, not on
/healthzactually returning 200 — add probes via a values override if your cluster needs them. - No NetworkPolicies / ServiceMonitors. Add your own if your cluster requires either.
self-hosted/helm/lettuce/values.yaml. Until the chart graduates,
expect to maintain a thin overlay (Kustomize or a wrapper chart) that
adds the missing pieces.5. Register your GitHub App
Lettuce talks to GitHub via a customer-registered GitHub App — we do not run a centrally-managed App against your repos (RFC-001 §6). You register the App on the GitHub instance you want to index, then paste its credentials into Lettuce.github.com
-
Go to
github.com/settings/apps/new
(for an org App, use
https://github.com/organizations/<org>/settings/apps/new). -
Homepage URL:
${LETTUCE_WEB_URL}. -
Callback URL:
${LETTUCE_WEB_URL}/api/v1/integrations/github/oauth/callback. -
Webhook URL: leave the App-level webhook empty. Lettuce
registers a per-repo webhook at add-repo time, pointing at
${LETTUCE_WEB_URL}/v1/github/webhook/{account_id}— that’s why the App needsWebhooks: Read & Write. -
Permissions:
Scope Access Repository — Contents Read Repository — Metadata Read Repository — Webhooks Read & Write Repository — Pull requests Read Repository — Issues Read -
Subscribe to events:
Push,Pull request,Issues. - Where can this App be installed: “Only on this account” is the safer default.
-
After creation, generate a private key (PEM) and copy:
- App ID
- App slug (the URL segment)
- OAuth client ID + client secret
- the PEM contents
GITHUB_APP_*env vars onapi+worker:Both pods need them — see §11’s “App is not configured” footgun.
GitHub Enterprise Server (LET-20)
Same shape, different host. On your GHES instance, register the App athttps://<your-ghes>/settings/apps/new with the same permissions and
events as above, then set:
NEXT_PUBLIC_GITHUB_HOST is a build-time variable — if you build
the web image yourself, set it as a build arg. The published
ghcr.io/uselettuce/web image is baked for github.com; GHES customers
who can’t rebuild should contact sales for a per-instance build.
Webhook URL on the App is still empty; Lettuce uses the per-repo hook
at ${LETTUCE_WEB_URL}/v1/github/webhook/{account_id}.
Full GHES runbook lives in
RFC-001 §10.
GitLab self-hosted (LET-21)
If you run a self-managed GitLab instance, point Lettuce at it with:${LETTUCE_WEB_URL}/v1/integrations/gitlab/callback. The required
scope is api (project read + webhook management).
Bitbucket Data Center is on the roadmap (LET-15.6) — not in v1.
6. Auth: bring your own IdP (LET-18)
Default is Supabase, which works fine if you don’t care. If you have a corporate IdP — Okta, Auth0, Azure AD, Keycloak, anything that speaks OIDC — switch to the generic backend:kid-based refresh on cache miss. See
src/codewaze/service/auth/oidc.py for the spec.
The web UI still expects Supabase login flows today. The OIDC
backend lets your API accept tokens minted by your IdP (useful for
agents and machine-to-machine), but the human-facing sign-in form is
not yet wired to OIDC. Tracked separately — file against LET-15.2 if
this blocks you.
7. Secrets — bring your own secret store (LET-23)
By default Lettuce reads secrets straight from env vars (LETTUCE_SECRET_STORE=env). That’s fine if your orchestrator already
injects secrets at runtime (Kubernetes Secrets, Compose env_file,
SOPS-encrypted overlays, etc).
If you’d rather pull secrets at boot from HashiCorp Vault, switch:
<mount>/data/lettuce/<name> —
the secret store reads keys lazily by their env-var name.
AWS Secrets Manager (LET-32) is also wired:
secretsmanager.GetSecretValue with the
literal env-var name as the SecretId. Rotate by updating the value
in Secrets Manager — Lettuce reads through on every fetch (no
process-level cache).
GCP Secret Manager is still pending as LET-33; the aws-style
switch (LETTUCE_SECRET_STORE=gcp) will land with that issue.
8. Observability (LET-25)
Lettuce ships OpenTelemetry tracing for HTTP, outbound httpx, andpsycopg SQL spans plus a per-job job.<kind> span on the worker.
Point at any OTLP-compatible backend:
9. Supply chain (LET-37)
Every image published toghcr.io/uselettuce/* on a tag build is
signed with cosign keyless (Sigstore OIDC — no private keys held
by Lettuce), ships an SPDX-JSON SBOM produced by syft, and is
scanned by grype. This is what unlocks the SOC 2 supply-chain
control, FedRAMP SR-3, SLSA, and most CISO-checklist questions.
Not yet exercised end-to-end. The signing / SBOM / scan steps live
in
.github/workflows/{compiled-image,publish-images}.yml but
ghcr.io/uselettuce does not exist yet (per the LET-17 note); the
first real run is on the next tag push after the org is created.Verify the image signature
Install cosign (brew install cosign or grab the static binary), then verify any image
Lettuce publishes:
api for worker, web, api-compiled, worker-compiled,
or mcp-compiled (LET-35 — the stdio MCP server + local-dev CLI image,
intended for the developer’s machine, not the Lettuce server). A successful
verify prints the Rekor public-log entry plus the workflow identity
that signed it; a failure exits non-zero. The
--certificate-identity-regexp pins the signer to a GitHub Actions
workflow under aviram-wba/coze — change that prefix if you fork.
Fetch the SBOM
Two equivalent paths — pick whichever your tooling prefers:-
Workflow artifact. Each tag build uploads
sbom-<svc>.spdx.jsonas anactions/upload-artifactartifact namedsbom-<svc>-<version>(90-day retention). Grab it from the run page on GitHub, or withgh run download. -
Cosign attestation. The same SBOM is attested to the image
under the SPDX predicate type, so it lives next to the image in
the registry:
cosign verify-attestationagainst the same image digest will prove the SBOM was signed by the same workflow identity that signed the image.
Vulnerability scan results
grype runs on every tag build with severity-cutoff: high, and its
SARIF is uploaded to GitHub code-scanning — findings land in the
Security → Code scanning alerts tab of the published repo, tagged
grype-<svc>. The scan is non-blocking on purpose: a brand-new
CVE disclosed five minutes before a release should not stop the
release, only inform whether to ship + patch or roll back. Operators
who want to gate their own deploys can fail their pipeline on the
grype SARIF directly.
10. Upgrade
Rolling (Kubernetes)
Blue/green (Compose)
Migrations
The API auto-applies the idempotentSCHEMA: list[str] block from
src/codewaze/service/db.py at boot. New releases extend that list —
the boot path runs each statement under IF NOT EXISTS / ALTER … IF NOT EXISTS guards, so re-running is safe.
Destructive migrations are not in v0. If a future release ever requires
one (column drop, type narrowing, etc), the release notes will call it
out and ship a separate lettuce-migrate step. Until then: upgrades
are pure image-tag bumps.
11. Troubleshooting
GitHub App is not configured on this Lettuce instance (HTTP 503
from /v1/integrations/github/..., also surfaces during repo-add)Your worker or api pod is missing one of GITHUB_APP_ID,
GITHUB_APP_NAME, GITHUB_APP_PRIVATE_KEY, GITHUB_APP_CLIENT_ID,
or GITHUB_APP_CLIENT_SECRET. The boot path logs the exact missing
vars (see config.github_app_env_or_warn).Most common cause: the env was set on the api pod but not the
worker pod (or vice versa). Both need all four. This is what LET-8
re-opened to make actionable — the error message is loud on purpose.License expired / LicenseExpired at bootYour JWT’s exp claim is in the past. Get a renewal from sales. The
cache file at /var/cache/lettuce/license.json is never trusted past
expiry — there’s no way to ride out a lapsed license.LETTUCE_LICENSE_KEY is required when LETTUCE_SELF_HOSTED=1Your license env vars didn’t propagate to the worker. Verify with:.env file must sit next to docker-compose.yml
and you have to start with docker compose --env-file … if you put it
elsewhere. Helm users: check existingSecret resolves and the Secret
has the right keys.No telemetry receipt verified in 7 days in the worker logsYour instance can’t reach https://telemetry.uselettuce.dev. Check
outbound firewall rules — egress on 443 to that host is required
unless your license is offline-capable (telemetry_required=false).
Run curl -fsS https://telemetry.uselettuce.dev/healthz from inside
the worker pod to confirm DNS + TLS work end-to-end.Webhook works on add-repo but Lettuce never sees a pushTwo common causes:
- Your GitHub App is missing
Webhooks: Read & Write— Lettuce silently logs the registration failure and falls back to the “manual webhook” card in the repo page. Re-grant the permission on the App, reinstall on the org, then click Retry webhook setup on the repo page. ${LETTUCE_WEB_URL}isn’t reachable from GitHub’s outbound IPs. This is GHES customers whose Lettuce sits inside a VPN — either put a reverse-proxy on the public internet or stop registering webhooks and clone on a schedule instead (not yet first-class — file feedback).
12. FAQ
Can I run Lettuce fully air-gapped? Not in v1. RFC-001 §1 explicitly defers air-gap to a paid SKU once a defense / finance customer signs. Telemetry + license checks both expect outbound HTTPS. If “no outbound” is a hard requirement, talk to sales — we’ll quote an Enterprise Air-gap pilot. Can I clone from a private GitLab self-hosted? Yes — LET-21 landed. SetGITLAB_HOST=gitlab.acme.com and register an
OAuth app on your GitLab instance (see §5).
Can I clone from Bitbucket Data Center?
Same answer — LET-15.6, on the roadmap.
Do I need Redis?
No. Lettuce uses Postgres LISTEN/NOTIFY for job queuing. One database
serves both api and worker.
Can I run multiple api replicas?
Yes — they’re stateless behind Postgres + S3. Compose users override
docker compose up -d --scale api=N; Helm users set api.replicas.
The worker also scales horizontally — each claims its own job under a
row-level lock.
What’s the disaster-recovery story?
Postgres backups + S3 versioning. The CODEWAZE_DATA_DIR volume on
each pod is a local scratch cache; everything durable lives in
Postgres + S3.
What’s the SLA?
TBD per RFC-001 §7.3. Today: best-effort business-hours email at the
Team tier, named CSM + business-hours email at Enterprise. 24×7 paid
support is a roadmap item — not yet sold.
What’s the pricing?
TBD per RFC-001 §7.1 — sales quotes against your team size and repo
count. Per-seat tiers with a bundled repo cap is the working
recommendation, but the founder hasn’t finalised.
Does the telemetry receiver work today?
The receiver (LET-30) ships and is live. The instance-side
sender (LET-30.1) is in flight — until it lands, the daily POST
isn’t emitted even with LETTUCE_TELEMETRY_ENABLED=true. License
verification still gates boot.
Where do I file bugs / feature requests?
Email founders@uselettuce.dev with your instance_id (visible on
the admin status page, also in the telemetry payload) and a docker compose logs / kubectl logs snippet. Linear-tracked issue tracker
goes public in v1.1.
Footnotes
-
The legacy
LETTUCE_TELEMETRY_DISABLED=1knob is still honoured as a deprecated alias for one release for backward compatibility; the worker logs a WARN naming the new var the first time it observes the old one set. ↩