Skip to content

Deploy GuestCraft with Vercel

Vercel deployments use its platform-set x-vercel-forwarded-for client-IP header by default (Vercel request headers). Do not override GUESTCRAFT_CLIENT_IP_HEADERS unless a trusted proxy overwrites the replacement header; then set GUESTCRAFT_TRUSTED_PROXIES to its exact ingress addresses or CIDRs and restrict direct access. An independent account lockout stops repeated guesses against one email even when addresses rotate: five attempts per 15 minutes, after which the lock ends on its own or a team manager lifts it in Users and roles. See the client address and locked sign-in.

GuestCraft’s source repository is private for now, so install it as the next section describes. The Deploy Buttons work once the repository is public: they clone it for a deployment. Keep credentials and guest data out of forks and deployment logs.

The button’s envLink points to the documentation site on GitHub Pages, https://spinny03.github.io/guestcraft/developer/deploy-secrets.html, which the docs workflow publishes from main once Pages uses GitHub Actions as its source. Until then, download deploy-secrets.html and open it locally in a modern browser. It makes no network requests. The four values are visible only in the tab and disappear on reload. The setup code is derived from AUTH_SECRET, so it is available before a Vercel project exists. Do not send secrets in a URL.

  1. Accept the owner’s GitHub invitation. The repository owner can select Spinny03/guestcraft directly at vercel.com/new/import in their own Vercel account. Vercel does not let a collaborator import a repository owned by someone else’s personal GitHub account. A collaborator can fork the repository privately into their own GitHub account, if the owner permits private forks, then import that fork from their own Vercel account. If a private fork is unavailable, use the CLI upload below. Authorize Vercel’s GitHub app only for the repository or private fork being imported.
  2. Before deploying the imported project, set Root Directory to apps/web, Framework Preset to Next.js and Node.js Version to 24.x. Generate new values in the offline secret generator and retain its setup code privately.
  3. Add a Neon or Supabase PostgreSQL database through the Vercel Marketplace storage integration for this project. Select the free plan if appropriate and review any provider terms and limits yourself. Choose a database region near the Vercel Functions region: Frankfurt for GUESTCRAFT_VERCEL_REGION=fra1, or a region near the default iad1. The build needs a reachable owner URL that keeps one PostgreSQL session for migrations (Neon’s unpooled URL, or Supabase’s session pooler) in addition to the runtime URL; do not type database credentials into source files.
  4. Add AUTH_SECRET, GUESTCRAFT_DATABASE_ROLE_SECRET, ENCRYPTION_KEYS and CRON_SECRET as Production environment variables using the values from the generator. For Frankfurt Functions, also add GUESTCRAFT_VERCEL_REGION=fra1. Enable Vercel system environment variables. Deploy to production, wait for the build and migrations to finish, then open /setup and enter the saved code. Sign in with the new operator account.

An invited collaborator can clone the private repository with their GitHub access and deploy from their own Vercel account without connecting the repository to Vercel. The owner can use the same route. Install Node.js 24 and Vercel CLI, then clone Spinny03/guestcraft with GitHub authentication and run these commands from the repository root. Replace guestcraft-my-install with a unique project name in your account:

Terminal window
vercel login
vercel project add guestcraft-my-install
vercel project update guestcraft-my-install --root-directory apps/web --framework nextjs
vercel link --yes --project guestcraft-my-install
vercel integration add neon --plan free_v3 --metadata region=fra1 --metadata auth=false --environment production --no-env-pull
vercel env add AUTH_SECRET production --sensitive
vercel env add GUESTCRAFT_DATABASE_ROLE_SECRET production --sensitive
vercel env add ENCRYPTION_KEYS production --sensitive
vercel env add CRON_SECRET production --sensitive
vercel env add GUESTCRAFT_VERCEL_REGION production
vercel deploy --prod

Choose the Vercel account or team containing the new project when prompted. The Neon command uses the Free plan and Frankfurt region available in Vercel CLI 59.1.4; check the CLI’s current plan and region options before running it. It attaches the database to the linked project and injects its connection variables. Paste each generated value only at its vercel env add prompt; enter fra1 for the region variable. Do not pass secrets with --value, put them in a URL, or commit the .vercel directory or local environment files. Run vercel deploy --prod from the repository root so Vercel can upload the whole workspace, build apps/web and migrate the production database. Open the resulting /setup URL and enter the saved code.

New installation with a public Deploy Button

Section titled “New installation with a public Deploy Button”
  1. Generate AUTH_SECRET, GUESTCRAFT_DATABASE_ROLE_SECRET, ENCRYPTION_KEYS, CRON_SECRET, and the setup code in the offline page. Save the four secrets in a password manager. Use new, independent values for each installation.
  2. When this repository is public, choose a Neon or Supabase button. The EU variants set Vercel Functions to Frankfurt (fra1). Choose the database’s Frankfurt or closest EU region in the Marketplace wizard. The other buttons use Vercel’s Washington (iad1) default; choose a nearby database region. Review any provider terms and free-plan limits before creating a database.
  3. Paste the four generated secrets into Vercel. Ensure the database integration supplies a reachable owner URL that keeps one session for migrations (Neon’s unpooled URL, or Supabase’s session pooler) as well as a runtime URL. GuestCraft derives the stable instance ID from VERCEL_PROJECT_ID and the HTTPS origin from VERCEL_PROJECT_PRODUCTION_URL. Vercel must expose its system environment variables (enabled for new projects). The root is apps/web; use Node.js 24. Set Production-scoped GUESTCRAFT_TRUSTED_ORIGINS to every additional address operators will open, including the project’s .vercel.app alias after choosing a custom production domain and any second custom domain. Origins are comma-separated exact HTTPS addresses, without a path or wildcard. The derived GUESTCRAFT_BASE_URL covers only the production domain, and passkeys work only there and on its subdomains: on the alias or a second domain, sign-in works but no passkey is offered, the startup log warns once naming each such address, and the organization security screen lists them. To offer passkeys everywhere, redirect the alias and any second domain to the production domain in the project’s domain settings instead of trusting them.
  4. The production build compiles Next.js, then runs numbered migrations and grants under a session advisory lock before deployment finishes. If migration fails, the deployment stops with a safe diagnostic; no setup code or database URL is printed. A preview build never migrates. The web runtime verifies its own migration checksums and accepts only later expand journal entries.
  5. Open your deployment’s /setup page and enter the code from the offline page. Create the first operator, company and workspace. Public sign-up remains closed. The setup code cannot be used after setup completes. Then sign in and configure a Channex connection in /onboarding when ready.

If the page is unavailable after deploy, check that the database integration supplied a runtime URL (DATABASE_URL or POSTGRES_URL) and a reachable migration owner URL (DATABASE_URL_UNPOOLED or POSTGRES_URL_NON_POOLING): the direct URL or a session pooler, not a transaction pooler. The latter needs CREATEROLE and permission to create GuestCraft schemas and grant on them. If Vercel’s system variables are disabled, enable them or set GUESTCRAFT_INSTANCE_ID and GUESTCRAFT_BASE_URL explicitly to stable values before redeploying. Do not change the instance ID on an existing database.

If sign-in on an alias says that the address is not configured, add that exact origin to GUESTCRAFT_TRUSTED_ORIGINS and redeploy, or redirect it to the base URL. For a preview, set Preview-scoped GUESTCRAFT_BASE_URL to its exact URL (passkeys then work on it) or add that URL to Preview-scoped GUESTCRAFT_TRUSTED_ORIGINS (sign-in without passkeys); a new preview URL needs a matching update. Keep the preview database and instance configuration separate from production.

The private CLI route was verified on 27 October 2026 with a disposable Vercel project and a free Frankfurt Neon database. The production build applied nine migrations; setup, sign-in, property and room creation, reservation and check-in persisted. /api/v1/health, /docs and /api/v1/docs returned HTTP 200. The temporary project and database were deleted after the test. That check predates the migration session check, which accepts Neon’s unpooled URL although Neon’s proxy announces its own cancellation key; the check is covered by tests with an equivalent proxy and with PgBouncer in session mode, and a hosted Neon re-run is pending. A public Deploy Button installation remains unverified while the source is private.

Terminal fallback while Pages is unavailable

Section titled “Terminal fallback while Pages is unavailable”

The offline HTML file is the preferred generator. This single local Node command prints equivalent fresh values and the setup code; keep its output private:

Terminal window
node -e 'const c=require("node:crypto"), A="0123456789ABCDEFGHJKMNPQRSTVWXYZ",s=()=>c.randomBytes(32).toString("hex"),auth=s(),role=s(),key=s(),cron=s(),bytes=c.createHmac("sha256",auth).update("guestcraft/setup-code/v2").digest().subarray(0,10);let bits=0,v=0,code="";for(const b of bytes){v=(v<<8)|b;bits+=8;while(bits>=5){bits-=5;code+=A[(v>>>bits)&31]}v&=(1<<bits)-1}console.log("AUTH_SECRET="+auth,"GUESTCRAFT_DATABASE_ROLE_SECRET="+role,"ENCRYPTION_KEYS=1:"+key,"CRON_SECRET="+cron,"SETUP_CODE="+code.match(/.{4}/g).join("-"))'

Use the Supabase Connect dialog to copy the real host, project reference and password. Migrations need one PostgreSQL session: use the session pooler on port 5432 (username postgres.<project-ref>), which is where the integration’s POSTGRES_URL_NON_POOLING usually points, or the direct host db.<project-ref>.supabase.co:5432 with username postgres. Supabase documents the direct host as IPv6-only unless the paid IPv4 add-on is enabled, and Vercel builds connect over IPv4, so the session pooler is the usual choice. GuestCraft refuses the transaction pooler on port 6543 for migrations, before connecting. The app and resident/cron worker may use the transaction pooler on port 6543; GuestCraft changes the owner username to gc_<instance-id>_app.<project-ref> or gc_<instance-id>_worker.<project-ref> and uses a derived password. It preserves the project suffix. Example shapes (placeholders only):

POSTGRES_URL=postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
POSTGRES_URL_NON_POOLING=postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:5432/postgres
# or, where the build can reach it:
DATABASE_URL_UNPOOLED=postgresql://postgres:<password>@db.<project-ref>.supabase.co:5432/postgres

Supabase’s postgres is not a superuser. The migrations install btree_gist and unaccent when absent; unaccent may already live in Supabase’s extensions schema. GuestCraft resolves its actual schema rather than assuming public. Supabase’s own API, Auth and Storage services are not used as GuestCraft application services.

A Deploy Button clones once. For maintainable updates, fork the public repository, connect your fork to Vercel, review upstream releases and SQL migrations, and merge upstream into your fork. Back up the database and retain the secret ring before a production deployment. Each production build reapplies idempotent role/grant setup and only pending migrations. Never point a preview at the production database: use a separate Supabase project/branch or a Neon database branch, with separate secrets and instance ID. Preview migrations are intentionally skipped by the build command, so provision and migrate the preview database separately before testing it.

Vercel invokes /api/internal/channex/drain daily at 03:00 UTC, and /api/internal/domains/check (the 24-hour checks of organizations’ proved domains, which ordinary traffic also wakes at most hourly) daily at 04:30 UTC, and adds Authorization: Bearer <CRON_SECRET>. The routes reject a missing or wrong secret. The daily cron is suitable for personal/demo recovery. A commercial hosted installation requires a 1–5 minute Pro cron or external scheduler: backoff, bounded unknown repair and killed Functions can exceed the request pump window. On Vercel production, committed reservation, inventory, room-block and rate changes, ARI activation/resync, and verified Channex signals schedule a pump immediately with Next.js after() in the same function. The HTTP response does not wait for publication or feed intake. Enable Fluid compute: the API route declares a 300-second maximum lifetime, and the pump uses at most 240 seconds minus the elapsed request time, including coalescing and cleanup headroom. Legacy Hobby functions cannot support this budget. The pump waits only while real delayed jobs fit its remaining budget (6-second availability and 45-second commercial gathering), and ends when the queue has no such work, using the same leases, booking barrier and database pacing as Docker. Preview and development request pumps are disabled. Backoff, unknown outcomes, sustained backlog, a missed webhook or host termination can still require a later invocation; requests cannot guarantee continuous worker latency. Hobby is restricted to personal, non-commercial use; a commercial PMS installation needs Pro or another host. Pro supports more frequent cron schedules; change apps/web/vercel.ts to * * * * * (every minute) or */5 * * * * (every five minutes) for a 1–5 minute safety cadence. A free external scheduler, such as cron-job.org, can call the same HTTPS endpoint every 1–5 minutes with a private Authorization: Bearer <CRON_SECRET> header. Store the secret in that scheduler’s protected configuration; never put it in the URL or logs. Vercel acknowledges an authorized scheduler request with 202 {"requested":true} and runs the drain with after(); this also fits cron-job.org’s 30-second HTTP limit. The response acknowledges scheduling, not completion; inspect sync health for outcomes. Direct/Docker calls without the host lifetime hook retain the synchronous drain response. Vercel does not retry failed cron invocations; the durable queue recovers jobs at the next drain. Docker retains its resident worker and does not start request pumps. Pump failures keep the durable queue and outbox available to that worker or the next cron invocation. See ADR 0034, Next.js after and Vercel function duration.

Before rotating AUTH_SECRET, for a legacy installation without an independent database secret, pin GUESTCRAFT_DATABASE_ROLE_SECRET to the installation’s original auth secret in both build and runtime environments and deploy once. Then set the new AUTH_SECRET and BETTER_AUTH_SECRETS=2:<new>,1:<old> while keeping the database secret unchanged. Prefer an independent database secret on fresh installations. Existing role passwords are never overwritten by a build. To rotate database credentials, stage a new GUESTCRAFT_DATABASE_ROLE_ID and independent secret in a candidate deployment, let its migrations and login checks succeed, verify it, then promote it. Keep the old pair until old processes have drained and the candidate is serving; retire it explicitly afterwards. A previous runtime with the compatibility check can cold-start after an unpromoted or partially failed candidate, or Vercel Instant Rollback, only when every extra committed journal entry is expand and the SQL/grants preserve its behavior. A contract entry needs roll-forward; Vercel Instant Rollback never rolls back SQL. The first rollout of a compatibility-aware runtime needs roll-forward readiness: earlier runtimes still have a strict check and fail cold starts after any newer migration commits. On Vercel, do not Instant-Rollback to such a deployment; if a build fails after migration commit, fix the environment and redeploy the same commit before promotion. To make a breaking change, ship an additive expand release, then switch code while retaining old objects, and ship contract cleanup only after old processes drain. See the full rotation and recovery procedure. The setup code changes, but completed setup is unaffected. To rotate ENCRYPTION_KEYS, prepend a new version and retain old keys until the Channex credentials, Channex webhook secrets, booking revision archives, guest messages, the signing secrets of outbound webhook endpoints and the secrets of payment provider connections are re-encrypted with the documented credentials:reencrypt command. Preserve old keys for database backups. Moving between Neon and Supabase requires a PostgreSQL backup/restore, both connection URLs updated together, the same instance ID and secret rings, and a production migration before serving traffic. Test the restored instance in isolation first; a database integration switch is not automatic data migration.

The development check used Supabase CLI 2.109.1 with PostgreSQL 17 and a transaction pooler. Create a disposable CLI project with ID gc-oneclick-supabase. In its supabase/config.toml, set [db].port=4081, [db].shadow_port=4082, [db].major_version=17, [db.pooler].enabled=true, [db.pooler].port=4083, and [db.pooler].pool_mode="transaction"; set the other service ports within 4080–4089. Start only the database and pooler:

Terminal window
supabase start --workdir /tmp/gc-oneclick-supabase -x edge-runtime,gotrue,imgproxy,kong,logflare,postgres-meta,postgrest,realtime,storage-api,studio,vector --yes

The tested local direct URL was postgresql://postgres:postgres@127.0.0.1:4081/postgres and the transaction pooler URL was postgresql://postgres.pooler-dev:postgres@127.0.0.1:4083/postgres (the CLI’s disposable default password and tenant). Set DATABASE_URL_UNPOOLED to the direct URL and DATABASE_URL to the pooler URL. The app rewrites the latter to gc_<instance-id>_app.pooler-dev and its derived password. Run the migration command twice to confirm the second run applies zero files. The built front-desk and team Playwright flows ran against these endpoints with a synthetic instance and secrets. Stop the local stack when finished. This is a manual check until a bounded CI job is added.

Hosted booking recovery uses the stable VERCEL_DEPLOYMENT_ID, persisted per connection. A changed deployment after a long gap starts automatic historical/future-booking catch-up; repeated invocations of the same deployment do not treat quiet periods as outages. Unknown ARI sends repair after a bounded settling wait; Channels exposes audited retry for stuck sends and capped item rejections.

A verified booking signal that remains undrained for 15 minutes, or deployment recovery that stalls for 15 minutes, prudently closes the affected property on every mapped channel. A daily cron alone cannot prevent that closure after a missed pump or redeploy. Configure a scheduler every 1–5 minutes for a live hosted property (Vercel Pro cron or an external HTTPS scheduler); daily Hobby cron is a demo recovery safety net. Local reconciliation reopens current inventory automatically. Channels names the workspace administrator as responsible for resolving booking intake failures.

Acknowledgement refusals are listed on individual revisions in Channels and retried independently. They do not hold or close inventory. ARI health also aggregates acknowledgement failures per property; external notifications remain planned.