Keel

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

  1. Install and copy the env file. bun install, then cp .env.example .env.local. See Setup for the full walkthrough.
  2. 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.
  3. Generate an auth secret. Run npx @better-auth/cli secret and paste the output into BETTER_AUTH_SECRET. Leave BETTER_AUTH_URL as http://localhost:3000 for now.
  4. Migrate and seed. bun run db:migrate && bun run db:seed. This creates the schema and a demo login (demo@keel.dev / password1234).
  5. Start it. bun run dev, open http://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".

  1. Name and tagline. SITE_NAME and SITE_DESCRIPTION in lib/seo.ts. These feed page titles, metadata, the OG image, and structured data.
  2. Logo. components/marketing/keel-logo.tsx is used in the site header, footer, auth and onboarding screens, and both sidebars (app/icon.tsx too). Swap the SVG, keep the export name or rename it everywhere with a find-and-replace.
  3. Favicon and app icon. app/icon.tsx and app/apple-icon.tsx generate them in code. Replace them, or delete both and drop an icon.png / apple-icon.png into app/.
  4. Social share image. app/og/route.tsx renders the OG card. Adjust colors and the wordmark.
  5. Colors and fonts. Color tokens live at the top of app/globals.css (light and dark blocks), fonts in app/layout.tsx. See Fonts & theme.
  6. Email sender and look. The fallback sender name is in lib/email.ts, the shared email header/footer is emails/components/email-layout.tsx.
  7. Your AI assistant's context. Update the product name and description at the top of AGENTS.md, then start logging your own decisions in memory/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.

  1. Home page. app/page.tsx lists the sections in order, each one is a file in components/marketing/. Delete the ones you don't need, rewrite the rest.
  2. 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.
  3. Keel-specific extras to remove. app/affiliates/, components/marketing/affiliate-link-generator.tsx, components/marketing/lemonsqueezy-affiliate-script.tsx (and its tags in app/page.tsx and app/pricing/page.tsx), and components/marketing/github-checkout-dialog.tsx only exist to sell Keel itself. They already stay hidden on any domain other than keelkit.dev (see IS_KEELKIT in lib/site.ts), so there's no rush.
  4. Links and contact details. The X/Twitter link in components/marketing/site-footer.tsx. The support address and GitHub links come from env vars: set NEXT_PUBLIC_SUPPORT_EMAIL (defaults to support@ your domain) and, if your repo is public, NEXT_PUBLIC_DOCS_REPO_URL to show "Edit this page on GitHub" under each doc and an "Open an issue" card on /contact. See lib/site.ts.
  5. Blog, changelog, docs. Blog posts are MDX in content/blog/, the changelog is app/changelog/page.tsx, docs are MDX in content/docs/ with the sidebar in lib/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 an updated: 2026-01-31 line, bump it when you change the page.
  6. Page metadata. Every public page builds its title, canonical URL, and share card with pageMetadata() from lib/seo.ts. Use it for any page you add so it gets its own OG image instead of the homepage's.
  7. Legal pages. content/legal/terms.mdx and content/legal/privacy.mdx ship 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 the warning prop from app/legal/terms/page.tsx and app/legal/privacy/page.tsx to 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 and RESEND_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_KEY from Stripe API keys, STRIPE_WEBHOOK_SECRET from Stripe webhooks. Webhook URL: {BETTER_AUTH_URL}/api/webhooks/stripe. No products to create, prices come from lib/billing/plans.ts. Activate the Customer Portal once in the Stripe dashboard. Subscribe the endpoint to the subscription and invoice events plus checkout.session.completed, checkout.session.async_payment_succeeded, charge.refunded, and charge.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_* or POLAR_* vars. See LemonSqueezy's developer guide or Polar's docs.
  • LemonSqueezy also needs order_refunded and Polar order.refunded enabled on the webhook, for the same credit clawback.
  • More detail in Plans & checkout and Webhooks.

AI chat (optional)

File uploads (avatars, team logos)

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):

sql
UPDATE "user" SET role = 'superadmin' WHERE email = 'you@yourdomain.com';

Sign out and back in, then visit /admin. See Admin access.

6. Deploy

  1. Create a production database separate from your dev one, and run bun run db:migrate against 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.
  2. Import the repo into Vercel and add your env vars there. See Vercel deploy and Vercel's env var docs.
  3. Point URLs at your domain. Set both NEXT_PUBLIC_SITE_URL and BETTER_AUTH_URL to https://yourdomain.com. Sitemap, canonical links, OG images, and auth callbacks all read these. The server refuses to start in production without BETTER_AUTH_URL and a 32+ character BETTER_AUTH_SECRET.
  4. Decide on HSTS preload. next.config.ts sends a Strict-Transport-Security header for your host only. Add includeSubDomains; preload only once every subdomain you own serves HTTPS and you plan to submit the domain to hstspreload.org, it's hard to undo.
  5. Update every callback URL you registered with a localhost address: Google and GitHub OAuth redirects, and your billing provider's webhook URL (which gives you a new production signing secret).
  6. Switch billing to live mode. Live keys and a live webhook secret, and POLAR_MODE=production if you're on Polar.
  7. 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.

  1. Sign up with a fresh email, and confirm the verification email arrives from your domain.
  2. Reset a password and use a magic link.
  3. Create a team, invite a second email, accept the invite from that inbox.
  4. 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.
  5. Send a message from the contact form.
  6. 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.
  7. Visit /sitemap.xml and /robots.txt and check they list your domain, not localhost.
  8. Visit a URL that doesn't exist and check you get the branded 404 page.
  9. Read your Terms and Privacy pages one more time.
  10. Confirm CI is green on your repo, see CI checks.
Last updated Sep 23, 2026