All posts

Walkthroughs / 2026-09-01

From clone to deployed, start to finish

Every step between `bun install` and a live domain — what you configure, in what order, and which steps you genuinely cannot automate.

From clone to deployed, start to finish

This is the whole path, in the order the dependencies actually run. Nothing here is optional except where it says so.

If you work with Claude Code, you can hand steps 1, 2 and 4 to the skills that ship with the repo (/setup, /google-oauth, /stripe or /creem, then /deploy). They run the same commands and check the result. Here is what they do, so you know what you're approving.

1. Secrets, database, run (5 minutes)

bun install
cp .dev.vars.example .dev.vars   # then set BETTER_AUTH_SECRET: openssl rand -base64 32
bun run db:migrate:local
bun run db:seed                  # optional: 90 days of demo data for the admin pages

.dev.vars is gitignored; never force-add it. The environment is validated at boot and Google's client id and secret are required, so do step 2 before bun run dev. If you want to look around first, placeholder values pass validation and the app starts. Only the Google button won't work.

2. Google OAuth (10 minutes, mostly browser)

Create an OAuth client in Google Cloud with redirect URI http://localhost:3000/api/auth/callback/google. Put the id and secret in .dev.vars, and the id also in appConfig.googleClientId: One Tap runs in the browser and needs it public. Then:

bun run dev

Google isn't the only way in. Emailed sign-in codes work locally without any email provider: until RESEND_API_KEY is set, the code is printed in the dev server's log. GitHub sign-in is optional; set its client id and secret and the button appears.

Sign in, then promote yourself:

bunx wrangler d1 execute DB --local \
  --command "UPDATE user SET role='admin' WHERE email='you@example.com';"

3. Make it yours (30 minutes, and worth every one)

src/config/app-config.ts first: name, canonical URL, support address, legal entity. Those values reach the marketing pages, every transactional email, and the Terms and Privacy pages — the legal entity is printed verbatim, so fill it in before you take a payment.

Then src/config/plans.ts. The comment at the top explains the ladder: packs sell at list price, subscriptions below it, and plans.test.ts fails the test suite if a tier ever drops under a margin floor. Set CREDIT_COST_CENTS from a measurement, not a guess.

4. Payments (20 minutes)

Stripe and Creem both work; PAYMENT_PROVIDER picks which one takes new checkouts, and both can be configured at once. With Stripe: create the catalog — one product per tier, one recurring price per interval — and paste the ids into plans.ts, replacing the REPLACE_ME placeholders. Put the secret key and webhook secret in .dev.vars and set PAYMENT_PROVIDER.

Test locally with the Stripe CLI forwarding to your dev server (Creem has its own signed local test in the /creem skill):

bun run stripe:listen

Buy something with a test card and watch the ledger move. Until a provider key exists the purchase buttons say so instead of starting a checkout, so this step is safe to defer.

5. Deploy (15 minutes)

wrangler d1 create shipkit            # id → wrangler.jsonc
wrangler r2 bucket create shipkit-files
bun run db:migrate:remote
grep -v '^#' .prod.vars | grep = | wrangler secret bulk
bun run deploy

Migrations are applied by hand, never from CI — a schema change is a deliberate, watched step, and the deploy job refuses to ship code whose migrations are still pending.

For a small product, all of this runs on the $5 Workers Paid plan. We worked out what it costs next to Vercel and Postgres at 1,000 and 10,000 users.

6. The things only you can do

A second Google OAuth client for the production origin. A webhook endpoint in the payment dashboard pointing at <your-domain>/api/webhooks/stripe. A verified sending domain in Resend. These are browser steps with no API worth scripting — which is exactly why the repo ships Claude Code skills that walk you through them and verify the result instead of pretending they do not exist.

More posts