All playbooksBooking Platform

Deploy to production

Put the booking platform online with Vercel, Supabase, Stripe webhooks, the maintenance cron and transactional email.

When to use it

The product works locally and needs its first production environment. The steps assume Vercel for hosting; any Node.js host works if it can call the cron endpoint.

Rules to keep

  • SUPABASE_SERVICE_ROLE_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET and CRON_SECRET are server-only: never prefix them with NEXT_PUBLIC_ and never commit their values.
  • supabase/seed.sql contains demo data: never run it in production.
  • A payment is confirmed only by the signed Stripe webhook.

Steps

  1. Database. Create the production Supabase project, then apply the migrations in order: supabase link --project-ref <ref> and supabase db push.
  2. Auth. In Supabase, under Authentication → URL Configuration, set the Site URL and the redirect URLs to the production domain.
  3. Hosting. Import the repository into Vercel and copy every variable from .env.example:
    • NEXT_PUBLIC_APP_URL is the public domain, without a trailing slash;
    • CRON_SECRET is a long random value, for example from openssl rand -hex 32.
  4. Stripe. Create a webhook endpoint to https://<domain>/api/stripe/webhook with the events checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.expired and charge.refunded. Copy its signing secret into STRIPE_WEBHOOK_SECRET and redeploy. Use test keys first and switch to live keys only after the checks below.
  5. Cron. GET /api/cron must run every 5–15 minutes with Authorization: Bearer $CRON_SECRET. On Vercel add a vercel.json:
    { "crons": [{ "path": "/api/cron", "schedule": "*/10 * * * *" }] }
    
    Vercel sends the header automatically when CRON_SECRET is set. Intervals shorter than a day need the Pro plan; Supabase pg_cron with pg_net is an alternative.
  6. Email. Verify the sending domain on Resend and set RESEND_API_KEY and EMAIL_FROM. Without them, notifications are only logged.
  7. First admin. Register from /register, then promote the user in the Supabase SQL editor: update public.profiles set role = 'admin' where id = '<user uuid>';

Checks after deploy

  • curl -i https://<domain>/api/cron without the header answers 401.
  • A paid test booking stays pending_payment until the webhook confirms it, and Stripe shows the webhook delivery as successful.
  • Cancelling that booking refunds it according to the service policy.

Done when

  • the verification commands pass locally;
  • the three checks above pass on the production domain;
  • no secret appears in the repository or in client bundles.