Docs / Getting Started / Launch checklist
Launch checklist
Everything between "I just bought Keel" and "real users are signing up", in the order you'll actually hit it. You don't need all of it on day one. Parts 1 and 2 get you running and looking like your product, the rest can wait until you're closer to launch.
For each service below you'll find the env var it fills and a link to that provider's own setup docs. Their dashboards change more often than this page does, so their docs are the source of truth for the clicks.
1. Get it running locally
- Install and copy the env file.
bun install, thencp .env.example .env.local. See Setup for the full walkthrough. - Create a Postgres database. Any provider works (Neon, Supabase, local Postgres). Paste the connection string into
DATABASE_URL. Neon's connection guide is the quickest route. - Generate an auth secret. Run
npx @better-auth/cli secretand paste the output intoBETTER_AUTH_SECRET. LeaveBETTER_AUTH_URLashttp://localhost:3000for now. - Migrate and seed.
bun run db:migrate && bun run db:seed. This creates the schema and a demo login (demo@keel.dev/password1234). - Start it.
bun run dev, openhttp://localhost:3000, sign in with the demo account.
Stop here if you just want to look around
Everything else in the app degrades gracefully. Missing email keys log to the console, missing AI keys show a "not configured" message, missing billing keys disable checkout. Nothing crashes. The one exception is a production build: BETTER_AUTH_SECRET and BETTER_AUTH_URL are checked when the server starts (see lib/env.ts), since running without them in production isn't safe.
2. Make it yours
The five-minute rebrand. After this the app stops saying "Keel".
- Name and tagline.
SITE_NAMEandSITE_DESCRIPTIONinlib/seo.ts. These feed page titles, metadata, the OG image, and structured data. - Logo.
components/marketing/keel-logo.tsxis used in the site header, footer, auth and onboarding screens, and both sidebars (app/icon.tsxtoo). Swap the SVG, keep the export name or rename it everywhere with a find-and-replace. - Favicon and app icon.
app/icon.tsxandapp/apple-icon.tsxgenerate them in code. Replace them, or delete both and drop anicon.png/apple-icon.pngintoapp/. - Social share image.
app/og/route.tsxrenders the OG card. Adjust colors and the wordmark. - Colors and fonts. Color tokens live at the top of
app/globals.css(light and dark blocks), fonts inapp/layout.tsx. See Fonts & theme. - Email sender and look. The fallback sender name is in
lib/email.ts, the shared email header/footer isemails/components/email-layout.tsx. - Your AI assistant's context. Update the product name and description at the top of
AGENTS.md, then start logging your own decisions inmemory/decisions.md. See Memory system.
3. Replace Keel's own marketing site
The public pages ship as Keel's real sales site, so you have a working, styled example of every section. Swap the content for your product's.
- Home page.
app/page.tsxlists the sections in order, each one is a file incomponents/marketing/. Delete the ones you don't need, rewrite the rest. - Pricing. Your plans, prices, seat counts, and credit packs are all in
lib/billing/plans.ts. The in-app billing page and the checkout both read from it. The public pricing components (components/marketing/pricing-*.tsx) describe Keel's own license tiers, rewrite them for your plans. - Keel-specific extras to remove.
app/affiliates/,components/marketing/affiliate-link-generator.tsx,components/marketing/lemonsqueezy-affiliate-script.tsx(and its tags inapp/page.tsxandapp/pricing/page.tsx), andcomponents/marketing/github-checkout-dialog.tsxonly exist to sell Keel itself. They already stay hidden on any domain other than keelkit.dev (seeIS_KEELKITinlib/site.ts), so there's no rush. - Links and contact details. The X/Twitter link in
components/marketing/site-footer.tsx. The support address and GitHub links come from env vars: setNEXT_PUBLIC_SUPPORT_EMAIL(defaults tosupport@your domain) and, if your repo is public,NEXT_PUBLIC_DOCS_REPO_URLto show "Edit this page on GitHub" under each doc and an "Open an issue" card on/contact. Seelib/site.ts. - Blog, changelog, docs. Blog posts are MDX in
content/blog/, the changelog isapp/changelog/page.tsx, docs are MDX incontent/docs/with the sidebar inlib/docs-nav.ts. Delete Keel's posts and docs, or keep the docs for your own team as internal reference. A doc shows "Last updated" only when its frontmatter has anupdated: 2026-01-31line, bump it when you change the page. - Page metadata. Every public page builds its title, canonical URL, and share card with
pageMetadata()fromlib/seo.ts. Use it for any page you add so it gets its own OG image instead of the homepage's. - Legal pages.
content/legal/terms.mdxandcontent/legal/privacy.mdxship with bracketed placeholders and a visible warning banner. Fill in every placeholder (company name, address, governing law, data processors) and have them reviewed before launch. Then delete thewarningprop fromapp/legal/terms/page.tsxandapp/legal/privacy/page.tsxto hide the banner.
4. Connect your services
Turn these on one at a time, each one is independent.
Email (verification, password reset, invites, magic links)
RESEND_API_KEY, from Resend's API key docs.RESEND_FROM_EMAIL, an address on a domain you've verified, see Resend domains. Leave it blank while testing, Resend's shared domain only delivers to your own inbox.CONTACT_INBOX_EMAIL, where the contact form sends messages. In production the form needs both this andRESEND_API_KEY, without them it shows visitors an error rather than pretending to send.
Sign in with Google / GitHub (optional, each pair is independent)
GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET, see Google OAuth setup. Redirect URI:{BETTER_AUTH_URL}/api/auth/callback/google.GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET, see creating a GitHub OAuth app. Callback URL:{BETTER_AUTH_URL}/api/auth/callback/github.
Billing (pick one with BILLING_PROVIDER)
- Stripe:
STRIPE_SECRET_KEYfrom Stripe API keys,STRIPE_WEBHOOK_SECRETfrom Stripe webhooks. Webhook URL:{BETTER_AUTH_URL}/api/webhooks/stripe. No products to create, prices come fromlib/billing/plans.ts. Activate the Customer Portal once in the Stripe dashboard. Subscribe the endpoint to the subscription and invoice events pluscheckout.session.completed,checkout.session.async_payment_succeeded,charge.refunded, andcharge.dispute.created(the refund and dispute events claw back credit packs). - LemonSqueezy or Polar: create one product/variant per plan and interval plus one per credit pack, then fill the matching
LEMONSQUEEZY_*orPOLAR_*vars. See LemonSqueezy's developer guide or Polar's docs. - LemonSqueezy also needs
order_refundedand Polarorder.refundedenabled on the webhook, for the same credit clawback. - More detail in Plans & checkout and Webhooks.
AI chat (optional)
ANTHROPIC_API_KEYfrom the Anthropic console, orOPENAI_API_KEYfrom the OpenAI dashboard. SetAI_PROVIDERonly if you add both. See AI providers.
File uploads (avatars, team logos)
UPLOADTHING_TOKEN, from your app's API Keys page. See UploadThing's quickstart.
Rate limiting (strongly recommended before launch)
UPSTASH_REDIS_REST_URL/UPSTASH_REDIS_REST_TOKEN, from the REST API section of a free Upstash Redis database. Without them, rate limiting on auth and AI routes is silently off, and the contact form only has a per-instance in-memory limit.
The full list with defaults is in Environment variables.
5. Give yourself admin access
The /admin panel is gated by a global superadmin role that no sign-up flow can grant. Sign up with your real email, then run this once against your database (Drizzle Studio via bun run db:studio, or your provider's SQL console):
UPDATE "user" SET role = 'superadmin' WHERE email = 'you@yourdomain.com';
Sign out and back in, then visit /admin. See Admin access.
6. Deploy
- Create a production database separate from your dev one, and run
bun run db:migrateagainst it. Don't run the seed script in production, it creates the demo login. On Supabase, also run the RLS lock script after every migration, see Seed & RLS. - Import the repo into Vercel and add your env vars there. See Vercel deploy and Vercel's env var docs.
- Point URLs at your domain. Set both
NEXT_PUBLIC_SITE_URLandBETTER_AUTH_URLtohttps://yourdomain.com. Sitemap, canonical links, OG images, and auth callbacks all read these. The server refuses to start in production withoutBETTER_AUTH_URLand a 32+ characterBETTER_AUTH_SECRET. - Decide on HSTS preload.
next.config.tssends a Strict-Transport-Security header for your host only. AddincludeSubDomains; preloadonly once every subdomain you own serves HTTPS and you plan to submit the domain to hstspreload.org, it's hard to undo. - Update every callback URL you registered with a
localhostaddress: Google and GitHub OAuth redirects, and your billing provider's webhook URL (which gives you a new production signing secret). - Switch billing to live mode. Live keys and a live webhook secret, and
POLAR_MODE=productionif you're on Polar. - Promote yourself to superadmin on the production database (step 5 again).
7. Before you announce it
Run through these on the live URL, not localhost.
- Sign up with a fresh email, and confirm the verification email arrives from your domain.
- Reset a password and use a magic link.
- Create a team, invite a second email, accept the invite from that inbox.
- Buy a plan with a test card, check the plan shows up in the app and in
/admin/subscriptions, then cancel it through the billing portal. - Send a message from the contact form.
- Open the home page share preview in a link debugger (paste the URL into a Slack or X draft) and check the OG image and title.
- Visit
/sitemap.xmland/robots.txtand check they list your domain, not localhost. - Visit a URL that doesn't exist and check you get the branded 404 page.
- Read your Terms and Privacy pages one more time.
- Confirm CI is green on your repo, see CI checks.