| name | peek-app-manifest-and-deploy |
| description | How to register, configure, and ship an app built on this starter kit — the app.json manifest (extendables, the registry settings URL, App Store listing), the Peek Development Hub, environment variables and secrets (PEEK_APP_SECRET / PEEK_APP_ID / PEEK_API_URL / PEEK_APP_URL), and deployment (Vercel + Neon recommended). Use when editing app.json, setting up env/secrets, registering the app with Peek, changing the embed/webhook URLs, or deploying. Triggers on "app.json", "manifest", "Development Hub", "deploy", "Vercel", "Neon", "database", "Supabase", "env vars", "secrets", "publish the app", "register the app", "extendable", "app-dev.json", "peek dev", "sandbox vs prod", "test app", "tunnel URL", "401 after deploy", "which manifest". |
Manifest, registration & deployment
Getting an app from this starter kit into a Peek Pro account has three parts: the manifest
(app.json) that declares what the app is and where it lives, registration in the Peek
Development Hub, and deployment to your host. This skill covers all three.
Hosting is your choice — this skill recommends a default, it doesn't mandate one.
Language/framework/SDK/test-runner are fixed by the kit; the host and database are the
"moving layer" you pick per project (research current best practice at build time). The
recommended default is Vercel (hosting) + Neon serverless Postgres (data, when you add
persistence), accessed server-side only via a DATABASE_URL, with any live UI driven by
polling/SSE from your own API routes — it stands up fast, pairs cleanly with Next.js, and
scales to zero. Swap it for any host/DB you prefer.
Why Neon and not Supabase? Supabase's value is a bundle built around its own auth —
Row Level Security keyed to a Supabase-signed JWT, client-direct DB access, Realtime gated by
RLS. This kit authenticates every request with Peek's token instead (verified server-side;
the browser never touches the DB directly — it calls your API routes, which scope data by
installDataId). So Supabase's differentiators go unused, and its third-party-auth path can't
even accept the Peek token (it requires asymmetrically signed JWTs; the peek-auth token is
symmetrically signed with PEEK_APP_SECRET). You'd end up using only the service-role key and
bypassing RLS — i.e. paying in complexity for features you can't use. Neon is just Postgres
(one server-side connection string, scope rows by installDataId), which is exactly the shape
this architecture needs. Use Supabase only if you have a specific reason to.
1. The manifest — app.json
app.json describes the app to Peek. Key fields (templated with {{APP_SLUG}} / {{APP_NAME}}):
app.id / app.name — the app slug and display name.
app_version — status, display_version, listing copy (listing_md, description),
icon_url, base_url, platforms, categories.
platform_extendables.peek — the platform capabilities the app requests. This kit ships
peek_backoffice_api@v1, which is what grants the app access to the back-office API
used via PeekAccessService (see peek-backoffice-api).
registry_extendables — how Peek surfaces the app. This kit ships
app_registry_settings_url@v1 with url: "/peek-pro/main" and
url_mode: "prepend_base_url" — i.e. Peek loads <base_url>/peek-pro/main (the embed entry
route) inside the iframe. This URL is what Peek POSTs to — it must match the embed route
(see peek-embed-and-auth). If you add a webhook, its endpoint URL is declared here too
(see peek-webhooks; pull the live doc for the exact registry keys).
When you change the embed path or add a webhook endpoint, update app.json to match, and
re-register/re-publish in the Development Hub.
The two-manifest / two-environment split — the #1 source of 401s
You are really juggling two separate Peek apps, and mixing their identities is the most common
cause of "every request 401s / blank iframe." Keep them straight:
| Source / production app | Dev / test app |
|---|
| Manifest | app.json (you author + publish) | app-dev.json (generated by peek dev, sits beside app.json) |
| App id | your real slug (e.g. guide-shift) | a test slug (e.g. guide-shift-test-dev) |
base_url | your deployed URL | the ephemeral tunnel URL (rewritten every peek dev run) |
| Installations API | prod (PEEK_API_URL default) | sandbox |
| id + secret live in | the host's env, set by you at deploy | .env.local, written by peek dev |
What @peektravel/app-cli dev actually does (so you know which app is live locally): it reads
your source app.json, then creates a distinct TEST app and writes app-dev.json next to it
(reused across restarts). From then on the dev loop targets that test app, not app.json: it
publishes the test app at the live tunnel URL and writes .env.local for you — PEEK_APP_ID
(test app), PEEK_APP_SECRET (test app), PEEK_APP_URL (tunnel), and PEEK_API_URL (sandbox).
app.json stays clean and never receives the tunnel URL. So under peek dev the app embedded
in the iframe is the app-dev.json test app — your env must be that test app's env.
Two invariants — break either and every request 401s:
PEEK_APP_ID / PEEK_APP_SECRET must belong to the same app whose tokens you verify. The
peek-auth token is signed with the issuing app's secret; your runtime verifies it with
PEEK_APP_SECRET (see peek-embed-and-auth). If the iframe is running the test app but
.env.local / the host still holds the prod app's id+secret (or vice versa), every verify
fails → 401, with nothing obviously wrong. This is exactly the ".env.local set for one app
while requests hit the other" trap.
PEEK_API_URL (sandbox vs prod) must match where the app is registered. The test app lives
in sandbox, the prod app in prod. PeekAccessService mints its own API tokens and calls
the installations API at PEEK_API_URL; point it at the wrong environment and the install isn't
found / calls fail. peek dev sets the sandbox URL for you; a production deploy must use the
prod PEEK_API_URL (the default) with the prod app's id+secret.
Rule of thumb: one coherent set at a time. {app id, app secret, API URL, base_url} must all
describe the same app in the same environment — never a prod secret against a sandbox API, or a
test-app id verifying prod tokens. Local dev = the test app in sandbox (managed in .env.local by
peek dev); production = your real app in prod (managed in the host's env). Don't cross the streams.
app-dev.json is generated and its base_url is rewritten every run — treat it as ephemeral dev
output, not source of truth (consider gitignoring it). You author and publish app.json; the
secret it writes to .env.local is already covered by the repo's .env* ignore.
2. Registration — the Peek Development Hub
- Get access to the Peek Development Hub.
TODO(verify) Hub URL + onboarding steps.
- Register the app; obtain its app ID (
PEEK_APP_ID) and shared secret
(PEEK_APP_SECRET — used to verify the peek-auth JWT; see peek-embed-and-auth).
- Confirm the sandbox environment and validate there before production. Never test against
a live account. How sandbox vs. production credentials/endpoints differ is a Development Hub
detail —
TODO(verify) it there or ask the user; don't assume.
- The app's runtime Peek auth is per-install via the token flow — there is no login the
developer or user creates. Registration just provisions the app's identity/keys.
3. Environment & secrets
Env is validated by lib/env.ts (Zod). Required:
| Var | What | Notes |
|---|
PEEK_APP_SECRET | Shared secret for verifying the peek-auth JWT | Secret — never commit. Must be the secret of the same app whose tokens you verify (see the two-manifest split above) |
PEEK_APP_ID | The app's ID / issuer | From the Development Hub. Must pair with the matching PEEK_APP_SECRET — same app |
PEEK_APP_URL | The app's own public base URL | Used to build the embed redirect target and dev origins |
PEEK_API_URL | Peek back-office API base | Defaults to the prod installations API (https://app-registry.peeklabs.com/installations-api). Sandbox uses a different URL — it must match the environment the app is registered in |
These four are not independent knobs — {PEEK_APP_ID, PEEK_APP_SECRET, PEEK_API_URL, PEEK_APP_URL}
must all describe the same app in the same environment (prod or sandbox). A prod secret with
a sandbox API, or a test-app id verifying prod tokens, 401s every request. peek dev keeps the
sandbox set coherent in .env.local; you keep the prod set coherent in the host's env.
Any var an app adds is required too. As a build introduces persistence or integrations
(DATABASE_URL, a webhook signing secret, third-party API keys), register each in
lib/env.ts's Zod schema so it's validated on boot, and add it to the production set. The
four above are the kit's baseline, not the whole list for a finished app. At the end of a
build, hand the user the complete, app-specific set (see the "Before deploy: env-var checklist"
step in peek-app-builder) — a var that's set locally but missing on the host is the most
common post-deploy failure (the app 500s on first request; see peek-embed-and-auth).
Secret hygiene: secrets live in your host's secret store — Vercel Environment
Variables by default (Project → Settings → Environment Variables), or the equivalent on
whatever host you choose — never in the repo or the client bundle. If you add Neon, keep the
DATABASE_URL server-only — the DB is reached only from your API routes, never the client
bundle (there is no client-direct DB access in this kit). No PII or tokens in logs. In CI, the
build uses placeholder env values (see
.github/workflows/ci.yml) purely to satisfy env validation — real values live only in the
deployment environment.
4. Deployment — Vercel (recommended default)
Next.js deploys to Vercel with zero Docker config — Vercel builds it natively. This is the
recommended default; any Node-capable host works.
- Connect the repo to a Vercel project (Import Git Repository).
- Set env vars in the project settings (the four in §3; the Neon
DATABASE_URL if used).
- Build/output is detected automatically for Next.js — no
Dockerfile needed on Vercel.
.github/workflows/ci.yml — on every branch: lint → typecheck → test w/ coverage →
build. Keep it green (see testing-peek-apps). This is host-agnostic and stays.
Data/persistence (when needed): Phase 0 ships no database. When you add one, Neon
(serverless Postgres) is the recommended default — create a project, take its DATABASE_URL
(server-only), connect from your API routes, and scope rows to installDataId
(see peek-backoffice-api). For live UI, poll or stream (SSE) from your own routes. Not
Supabase — its auth/RLS/Realtime model is built around its own JWT and can't consume Peek's
(symmetrically signed) token, so you'd use none of it; see the "Why Neon and not Supabase?"
note at the top.
Ignore the Fly.io files. Dockerfile, fly.toml, and fly-deploy.yml are placeholder
scaffolding scheduled for removal — don't build a Fly deploy around them.
First-time deploy checklist:
Related skills
- peek-embed-and-auth — the embed route the manifest points at; how
PEEK_APP_SECRET is used.
- peek-backoffice-api — the
peek_backoffice_api@v1 extendable grants this API access.
- peek-webhooks — webhook endpoint URLs are declared in the manifest/registry too.
- testing-peek-apps — the CI gate that runs before deploy.