Development Guide
Set up a Teak checkout, run the app against the shared development deployment, and work in worktrees and cloud VMs
Prerequisites: Git plus Bun and Node.js at the versions pinned in package.json (packageManager and engines.node). The pins are exact: setup and doctor warn on a different patch version and refuse a different minor or major one. CI reads the same pins (bun-version-file and node-version-file), so local and CI never disagree. Node is pinned beside Bun because Convex Node actions run on the system Node.

To see how the pieces fit together first, read Architecture.
Quick Start (local)
Clone the repository
git clone https://github.com/praveenjuge/teak.gitMove into the project
cd teakStart the app
bunx convex login
bun run devThe first run installs dependencies and writes the local env files. It then starts the web app against Teak’s shared Convex development deployment, signed in through WorkOS staging, and creates a sign-in account for this checkout seeded with sample cards. Uploads, previews, AI metadata and the other backend features work as they do on that deployment. When it’s ready it prints the URL and the sign-in. It needs access to the Teak Convex project; without it, see Without access to the development deployment.
Check the setup
bun run doctorAccess Points
- Web app: http://localhost:3000 (each git worktree gets its own ports;
bun run dev --statusprints them) - Docs (proxies
/apiand/mcpto the development deployment): http://localhost:3001 - iPhone, iPad and Mac app with its Safari extension: Xcode (
apps/apple) - Android app: Gradle (
apps/android)
See Architecture for the monorepo layout.
Core Commands
bun run dev # Set up, then start the web app on the development deployment, seeded
bun run dev --push # Same, and push this checkout's backend code
bun run dev --detach # Start in the background and return once ready (combine with --push)
bun run dev --local # Same app on a local backend, WorkOS emulator and Core Worker (combine with --detach)
bun run dev --status # This checkout's ports, URLs and sign-in, and whether it pushes
bun run dev --stop # Stop this checkout's stack
bun run setup # Set up only (--target for other surfaces)
bun run doctor # Validate the environment (--json --target for agents)
bun run dev --all # All services
bun run smoke:web # Headless sign-in check against the running stack
bun run audit:env # Environment contract + local dotenv audit
bun run dev convex # Convex backend only
bun run dev extension # Chrome extension
open apps/apple/Teak.xcodeproj # iPhone, iPad and Mac app, Safari extension
bun run dev raycast # Raycast extension
bun run dev docs # Docs site
bun run build # Production build (all)
bun run build:extension # Package Chrome extension
bun run build:raycast # Build Raycast extension
bun run lint && bun run typecheck
bun run test # Unit tests (turbo; test:unit / test:edge for Convex splits)
bun run verify # typecheck + lint + test (affected)
bun run --cwd packages/tests e2e # The end-to-end suite, on its own local stack
bun run check # Quality report (Ultracite)
bun run fix # Auto-fix issues
bun run pre-commit # Pre-commit checks
bun run publish:raycast # Publish to Raycast store
bun run clean # Clear Turborepo caches
Testing covers which tests to write and how to run each suite.
Dependencies
A package that two or more workspaces depend on takes its version from the catalog in the root package.json, and each workspace lists it as "catalog:". To move one of these packages to a new version, change its range in the catalog and run bun install. When a second workspace starts using a package, add the package to the catalog with bun add --catalog. Two workspaces write out ranges instead:
apps/cliis published withnpm publish, which doesn’t resolvecatalog:, so its ranges match the catalog’s.apps/raycastinstalls from its own npm lockfile for the Raycast store, so its versions move on their own.
scripts/repo/repo-hygiene.test.ts checks all of this.
The shared development deployment
Every checkout, worktree and cloud VM runs against one Convex development deployment, dev:reminiscent-kangaroo-59, with its own WorkOS staging environment, Core Worker and storage. Files, previews, AI metadata, screenshots, imports and exports, WorkOS Connect, and Polar sandbox checkout all work there. When you’d rather not share it, bun run dev --local runs the app on a local backend instead.
Reaching it
- Mac checkouts (the main checkout and every worktree) use your Convex login: run
bunx convex loginonce. Setup selects the development deployment inpackages/convex/.env.local. - Cloud sessions (Claude Code on the web, Codex, Amp) have no login. Their environment’s secrets hold
CONVEX_DEPLOY_KEY, a dev deploy key scoped to the development deployment. It is the only secret a cloud environment needs.
Setup and doctor refuse every other key or selection (production, preview, project and admin keys, other deployments), so development never touches production. The WorkOS staging client ID and API key the web app needs are read from the development deployment into apps/web/.env.local; nobody exports them by hand.
One backend, one pusher
Every checkout shares the deployment’s code and data, so only one checkout at a time runs convex dev. It holds the push lease, stored in the deployment (packages/convex/devPushLease.ts) and renewed every 30 seconds.
bun run devtakes the lease when it is free. Otherwise it runs the web app only, against whatever backend code is live, and says which branch pushed it.bun run dev --pushtakes the lease over. The previous holder stops pushing at its next heartbeat and says why.- A holder that stops (Ctrl-C, crash, closed laptop) frees the lease within two minutes.
Backend changes on a branch are live for every checkout while that branch holds the lease, so push a branch’s backend only while you work on it. bunx convex run devPushLease:status in packages/convex shows which branch and commit are live.
Signing in
Each checkout gets its own WorkOS staging account, dev+<machine>-<checkout>@example.org, with a random password kept in the ignored .agents/.state/dev-account.json. Its vault is seeded once with about 30 sample cards (packages/convex/devSeed.ts). bun run dev --status prints the email and password. For a session without a browser, use scripts/lib/workos-test-session.ts, as bun run smoke:web does.
A Claude Code cloud session uses dev+cloud-<hash>[email protected] instead, named after its session ID, so each session starts with a fresh vault. Those accounts are deleted two weeks after their last sign-in.
Upgrade to Pro opens a Polar sandbox checkout in a new tab for these accounts too. Polar refuses example.org addresses, so on the development deployment the Polar customer is created as teak-dev+<teakUserId>@teakvault.com, with the real address in its metadata (packages/convex/billing.ts). Subscriptions still reach the account because Polar customers map to users by customer ID, not email. Production always uses the account’s own email.
Worktrees
Every linked worktree leases its own block of ports the first time it runs, recorded in the repository’s git directory (scripts/dev/worktree-env.ts), so web apps in different worktrees run side by side. The main checkout keeps web 3000, docs 3001 and extension 3003. bun run dev --status prints the ports.
packages/convex/convex.json registers http://localhost:*/callback, which covers every one of those web ports, as a sign-in callback on the development deployment’s WorkOS environment, and lists each port as a CORS origin. The deployment’s API and MCP links (appUrl, settingsUrl) point at TEAK_DEV_APP_URL (port 3000), so links from a worktree’s API responses open the main checkout’s web app.
A local backend
bun run dev --local (add --detach to return once it’s ready) runs the web app on the end-to-end suite’s stack instead of the shared deployment: a local Convex backend that pushes this checkout’s packages/convex as you edit it, the WorkOS emulator, and the Core Worker in Miniflare. It needs no Convex login or deploy key, and nothing you do there reaches another checkout, so it suits schema changes and destructive work, and checkouts without access to the development deployment.
It creates dev+local-<checkout>@example.org on the emulator with a new password each time it starts, and seeds its vault with the same sample cards. bun run dev --status prints the sign-in. Link fetching, screenshots, transcripts, embeddings, WorkOS Connect and billing don’t run there (What the local stack doesn’t cover). It takes over packages/convex/.env.local; after bun run dev --stop, the next bun run dev selects the development deployment again.
Supported Targets and Limitations
bun run setup --target <name> provisions one surface. Every surface uses the
shared development deployment and its WorkOS staging environment; setup reads
the WorkOS client ID and API key from the deployment, so nothing is copied by
hand.
| Target | Needs | Limitations |
|---|---|---|
web |
Access to the development deployment | Only one checkout at a time pushes backend code (see One backend, one pusher) |
docs, cli |
Nothing beyond install | No Convex provisioning |
extension |
Access to the development deployment | Env files derive VITE_* aliases automatically |
core-worker |
bun run sync:cloudflare-dev --isolated afterwards |
Worker secrets are owned by the sync command, not setup |
iOS simulators, device builds, the Mac app, Xcode, and Safari stay on macOS. The Android app needs the Android SDK.
Without access to the development deployment
bun run setup and bun run dev work only against the development deployment;
they refuse any other deployment, so ask the repository owner for access. Until
then you can still run the app on a local backend
(bun run dev --local), the unit tests (bun run test) and the
end-to-end suite
(bun run --cwd packages/tests e2e), none of which need credentials. Running Teak on a deployment of your own is
Self-Hosting, which the development commands don’t configure.
Cloud agents
.agents/setup prepares a fresh Linux VM: the pinned Node and Bun, packages, the end-to-end suite’s browsers and, when CONVEX_DEPLOY_KEY is present, the wiring to the development deployment. Without the key it still prepares the VM for bun run dev --local, unit tests and the end-to-end suite.
- Claude Code on the web: set the environment’s setup script to
bash .agents/setupso the result is cached. Setup scripts there don’t receive the environment’s secrets, so the SessionStart hook in.claude/settings.jsonfinishes the job each session: it runs.agents/setupon a VM that isn’t ready, connects the checkout to the development deployment, reinstalls packages whenbun.lockhas moved past the cached snapshot, warms the Turbo cache in the background once per VM, and printsbun run dev --status. - Codex cloud: set the environment’s setup script to
bash .agents/setup. - Amp: runs
.agents/setupas the orb setup script and.agents/resumeon resume.
Give the environment network access to Convex, WorkOS and Polar (and Playwright’s download host for setup), then run bun run dev --detach. bun run dev --local --detach needs neither the key nor those hosts.
Environment and configuration
Every environment value has one owner and one classification. Names and metadata live in scripts/env/env-contract.ts; values are never committed, printed, or pasted into docs.
Where values come from
setup, doctor, dev and audit:env run with Bun’s automatic dotenv loading turned off (--no-env-file). They read only:
- Explicit shell exports (CI,
export, a command prefix). - The dotenv files declared for the command’s target in
scripts/env/env-targets.ts, read throughscripts/env/env-loader.ts. - Provider stores: the Convex dashboard, Vercel, Wrangler, GitHub.
- Files setup writes from canonical facts, which are never edited by hand.
Precedence is top-down: a shell export wins over a dotenv file, and a dotenv file wins over a generated default. Setup never overwrites a value a person set. It does refresh what it generated: loopback URLs left by an earlier end-to-end run or other ports, and the WorkOS credentials it syncs from the deployment. Because shell exports win, don’t export CONVEX_DEPLOYMENT, WORKOS_* or NEXT_PUBLIC_* in your shell.
Canonical facts and generated aliases
CONVEX_URL and CONVEX_SITE_URL are the canonical deployment facts, and SITE_URL is the canonical app origin. Framework aliases (NEXT_PUBLIC_*, VITE_PUBLIC_*) are generated from them by setup (scripts/env/env-aliases.ts). Frameworks need the aliases at build time, but nobody maintains them by hand.
Root .env* files are legacy: supported commands ignore them. scripts/env/dotenv-audit.ts still scans their names for production selectors, because editors and other tools may load them. Put new values in a shell export or the target’s declared file. Teak never deletes or rewrites your dotenv files.
Classifications
secret: a credential; its name and shape are validated, and it is never logged.public-config: a non-secret operator value with one provider owner.generated: derived from a canonical fact or the active deployment.build-metadata: derived from provider metadata, such as the commit SHA or version.platform-binding: provisioned by the platform, not supplied by an operator.
Classification is derived from the contract’s metadata (scripts/env/env-classify.ts), so it can’t drift from the declared fields. Values that are the same in every deployment are typed code with an optional override, not configuration.
Diagnostics
bun run audit:env: the contract-versus-repository audit plus the dotenv audit.bun run scripts/env/dotenv-audit.ts [--json]: local dotenv state, names only.bun run doctor --target <t> --json: readiness with stable check IDs, remediations, and redacted output safe to attach to a bug report.bun run scripts/env/env-contract-report.ts [--json]: baseline metrics and per-target manual-supply counts.
Reporting Setup Problems
bun run dev prints the failing setup check and how to fix it. When you report a problem, paste the redacted JSON report. It contains check names and remediations but never secret values:
bun run doctor --target web --json
Setup Checklist
Convex and sign-in
Teak signs in with WorkOS AuthKit everywhere: production, local development, CI, and self-hosted deployments. There is no other provider and no switch.
Locally, the web app signs in through the WorkOS staging environment that the development deployment uses, never production. Setup reads WORKOS_CLIENT_ID from the deployment and writes it into apps/web/.env.local, alongside NEXT_PUBLIC_WORKOS_REDIRECT_URI (the app origin plus /callback) and a generated WORKOS_COOKIE_PASSWORD. The web app signs in as a WorkOS public client, so it never holds WORKOS_API_KEY; the dev account and bun run smoke:web read that key from the deployment. Setup refuses production keys, and it stops if a shell export disagrees with the deployment. The end-to-end suite signs in through the local WorkOS emulator instead.
convex.json declares the AuthKit redirect URIs and CORS origins. One wildcard redirect URI, http://localhost:*/callback, covers every worktree web port; CORS origins can’t use wildcards, so each port is listed. For cloud development deployments, Convex applies them to the deployment’s WorkOS environment. The sign-out redirect and JWT template are not in convex.json; set them once per environment as in Set up WorkOS, which also lists the WorkOS CLI commands for them.
The CLI, browser extensions, Raycast, and MCP sign in through WorkOS Connect; the native apps use AuthKit directly. To sign them in against your development deployment, configure Connect and set WORKOS_AUTHKIT_DOMAIN and the WORKOS_CONNECT_*_CLIENT_ID values as in Connect. Set WORKOS_RESOURCE_ORIGIN to the deployment’s site URL, such as https://<deployment>.convex.site, and register <origin>/api and <origin>/mcp as resource indicators in its WorkOS environment. Token audiences then match the deployment’s own MCP URL instead of production’s.
bun run doctor fails until the deployment and the web app both have the client ID and API key, checks that the client, key, cookie password, and callback fit together, and warns about development settings the deployment’s owner hasn’t set yet.
Social sign-in, such as Google or Apple, is configured inside WorkOS, not in Teak.
Public API and MCP
The public REST API, MCP endpoint, discovery routes, OpenAPI spec, and SDK live in packages/convex. The development deployment serves the HTTP routes; push this checkout’s backend first if you changed them, then use the docs dev proxy for local API and MCP URLs.
Example:
bun run dev convex
The docs dev server proxies /api, /mcp, and OAuth protected-resource metadata to the Convex dev deployment. That gives local docs, examples, and smoke tests the same path shape as production: /api/v1 and /mcp.
The CLI, browser extensions, and Raycast sign in through WorkOS Connect and disconnect with POST /v1/oauth/disconnect (see Disconnect an OAuth connection).
A change to the public API ships on MCP, the SDK and the CLI in the same pull request; see Adding a Feature.
MCP Registry
The repo includes server.json for the MCP registry under the com.teakvault/mcp namespace, which the registry checks against a DNS record on teakvault.com. Each version tag publishes it through the MCP Registry Release workflow, which signs in with the domain’s Ed25519 key from the MCP_PRIVATE_KEY secret. To publish by hand with mcp-publisher:
mcp-publisher login dns --domain teakvault.com --private-key "$PRIVATE_KEY"
mcp-publisher publish
Environment Variables
See Self-Hosting → Environment settings for every env var required across web, mobile, extension, and the Convex backend.
Optional development URL overrides on the deployment: TEAK_DEV_APP_URL (default: http://localhost:3000), TEAK_DEV_API_URL (default: the development deployment’s site URL), TEAK_DEV_DOCS_URL (default: http://localhost:3001). The shared development deployment leaves them unset.
Pull requests
Teak’s repository is public, so every pull request title, body, comment and linked image is public. Fill .github/pull_request_template.md and follow these rules for anything you post on GitHub.
- Title: a conventional commit,
type(scope): outcome, such asfix(web): steady grid selection. The scope is the workspace (web,extension,convex,docs…). - Body: fill the template’s sections in order, and delete the ones that don’t apply and every HTML comment. Lead with the user-visible outcome, then what a reviewer needs to read the diff. Report verification faithfully: the commands you ran and what they returned, what you drove in the running app, and anything you couldn’t check and why.
- Never include secrets of any kind (name a new env var, never its value); real user data such as emails, card content, IP addresses, analytics IDs or production account IDs (describe counts and patterns instead); names of people, employers, clients or other companies and their internal products (services Teak already integrates with are fine); links a reviewer can’t open, internal hostnames or local paths; or raw logs you haven’t trimmed and redacted.
- Screenshots and video: capture only the local or development stack, signed in as the seeded account. Look at every frame before you link it, and recapture when one shows other tabs, bookmarks, notifications, devtools with cookies or headers, a terminal with env output, or anything above. Cover the states the change touches (empty, loading, populated, error; light and dark; 390px and 1440px). For API, CLI and MCP changes, use captured requests and output with tokens replaced by
<redacted>. Don’t commit screenshots to the branch only to show them in the pull request.
Before you post, re-read the title and body as a stranger would, and tick the template’s “Safe to publish” boxes only once each one is true.
Maintainer setup
The repository owner does these once on the development deployment; nobody else needs to:
- Set
TEAK_DEV_DEPLOYMENT=trueon the development deployment (bunx convex env set TEAK_DEV_DEPLOYMENT trueinpackages/convex, or the Convex dashboard). It turns on seeding and the push lease. - For cloud sessions, create a dev deploy key for the development deployment (
bunx convex deployment token create cloud-agents --deployment dev, or the deployment’s settings in the dashboard) and add it to each cloud environment’s secrets asCONVEX_DEPLOY_KEY. The key has full access to the development deployment, including its variables (the shared Files and WorkOS staging credentials), so keep it out of untrusted environments.
bun run doctor warns when TEAK_DEV_DEPLOYMENT is missing; until it is set, bun run dev still runs the web app but doesn’t seed or push.