Public Site Deployment
The public marketing and docs site is an Astro hybrid site in packages/site. It is separate from the Randal runtime, dashboard, Railway deployments, and self-hosted randal serve process.
Use this guide when you want a hosted marketing/docs preview or production site. Do not use it to deploy an agent runtime.
What Gets Deployed
- Source package:
packages/site - Build command:
bun run site:build - Output mode: Astro hybrid output with
@astrojs/vercel/serverless - Static pages: marketing/docs pages are prerendered by Astro
- Server route:
/api/personalizeis a Vercel serverless function when classifier personalization is configured - Server routes:
/api/auth/*and/api/account/*back web sign-up, sign-in and plan purchase (see "Accounts And Plan Purchase" below) - Server-rendered pages:
/signup,/login,/plans,/account,/account/passwordand/auth/callbackread the session cookie and cannot be prerendered - Docs content source:
README.mdand allowlisteddocs/*.mdfiles read at build time
The site does not run the Bun/Hono agent runtime in production. Vercel serves prerendered pages from its output and runs only the site API routes that are explicitly marked server-side.
Separation From Runtime Deployments
The public site is only the marketing/docs surface:
- It does not start
randal serve. - It does not host the dashboard or gateway runtime.
- It does not require
RANDAL_API_TOKEN, Discord secrets, LiveKit, Twilio, or Railway variables. - It only needs a model provider key if generated landing-page classification is intentionally enabled.
- It does not change self-host or Railway runtime deploy behavior.
- The site's PostHog snippet, when enabled, is included only in
packages/sitestatic pages. (Other surfaces have their own keys and transports — seedocs/telemetry.md.)
Use docs/deployment-guide.md for local, Railway, Docker, and imported-service runtime deployments.
Recommended Vercel Setup
Use the Vercel GitHub integration for PR previews and production deploys. This keeps site deploys in Vercel and avoids a duplicate GitHub Actions deploy path.
- Create or log in to a Vercel account, ideally under the Hassion Studio team or org.
- Import the GitHub repository
Hassion-Studio/randal. - Configure the Vercel project with these settings:
| Setting | Value |
|---|---|
| Framework preset | Astro |
| Install command | bun install |
| Build command | bun run site:build:vercel |
| Output mode | Vercel Build Output API via the Astro Vercel adapter, promoted from packages/site/.vercel/output to .vercel/output |
| Node runtime | Node 20 (package.json sets engines.node to 20.x) |
The repo includes vercel.json with the same settings. The extra Vercel build script exists because the Astro app lives in packages/site, while Vercel's Build Output API expects .vercel/output at the deployed project root. Normal self-host users can ignore this file; it is inert unless the repo is connected to Vercel or a deploy command is run.
For local verification, use Node 20 before running bun run site:build if you need generated Vercel function metadata to match production. The adapter derives the function runtime from the Node process that runs the build.
Environment Variables
Copy packages/site/.env.example for local site development or set the same variables in Vercel.
| Variable | Required | Description |
|---|---|---|
PUBLIC_SITE_URL | Recommended | Canonical site origin, for example https://randal.bot or a Vercel preview URL. |
PUBLIC_POSTHOG_ENABLED | Optional | Set to true to enable PostHog on the public site. Any other value disables it. |
PUBLIC_POSTHOG_KEY | Optional | Public PostHog project key. Analytics stay disabled if this is missing. |
PUBLIC_POSTHOG_HOST | Optional | PostHog ingest host. Defaults to https://us.i.posthog.com; use the EU host if needed. |
OPENROUTER_API_KEY | Optional | Server-only key for landing-page classifier personalization. Leave unset to use static presets only. |
RANDAL_PERSONALIZATION_MODEL | Optional | Classifier model ID. Defaults to openai/gpt-4.1-mini. |
PUBLIC_TURNSTILE_SITE_KEY | Required with Turnstile | Public Cloudflare Turnstile site key for protected classifier requests. |
TURNSTILE_SECRET_KEY | Required in production if classifier is enabled | Server-only Turnstile secret. When set, /api/personalize verifies tokens server-side before any model call. |
RANDAL_PERSONALIZE_ALLOW_UNPROTECTED | Emergency/internal only | Set to true only to intentionally allow production classifier requests without Turnstile. Default behavior fails closed. |
RANDAL_SITE_SESSION_SECRET | Required in production for accounts | 32+ character secret that encrypts the session cookie. Generate with openssl rand -base64 32. The site refuses to serve any account page in production without it; local dev falls back to a known development key. |
PUBLIC_SUPABASE_URL | Optional | Hosted GoTrue origin. Defaults to https://auth.randal.bot — the same identity the desktop and mobile apps use. |
PUBLIC_SUPABASE_ANON_KEY | Optional | Supabase publishable key. Defaults to the production key the apps already ship. |
RANDAL_PROXY_URL | Optional | Metering proxy origin for billing and usage. Defaults to https://proxy.randal.bot. |
Do not set runtime secrets such as RANDAL_API_TOKEN, DATABASE_URL, a runner's OPENROUTER_API_KEY, legacy migration keys, or any agent runtime credential on the public site project. If OPENROUTER_API_KEY is set for the landing classifier, keep it server-only and configure Turnstile protection or the explicit unprotected override.
Accounts And Plan Purchase
randal.bot signs people up and sells plans directly. It is the same account as the apps: the same hosted GoTrue (auth.randal.bot), the same metering proxy, the same Stripe subscription. Someone who buys on the web downloads the app and signs in — nothing is re-purchased.
How the session is held
The browser never holds a token. Auth happens in same-origin API routes (/api/auth/*), which seal the GoTrue access and refresh tokens into an httpOnly, Secure, SameSite=Lax cookie encrypted with RANDAL_SITE_SESSION_SECRET (AES-256-GCM). Every billing call goes out from the server with the Bearer attached (/api/account/* → the metering proxy), so a marketing-site XSS has nothing to steal and the proxy needs no CORS policy for browsers.
Social sign-in uses PKCE, not the implicit fragment flow, for the same reason: GoTrue returns a ?code= that /auth/callback exchanges server-side. Email confirmation and password-recovery links still arrive as URL fragments (that is how GoTrue's own /verify redirect works), so /auth/callback also has a small client-side path that posts those tokens to /api/auth/adopt, which verifies them against GoTrue before sealing anything.
One consequence of the server-side design needs naming: GoTrue now sees Vercel's egress IP on every call, not the visitor's, so its own per-IP rate limiting can no longer slow a password-guessing run. src/server/auth-rate-limit.ts puts that limit back where the real IP still exists — 10 credential attempts and 4 recovery emails per IP per minute. It is per serverless instance and therefore best-effort; GoTrue's limits and password rules remain the backstop.
One-time setup
-
Site: set
RANDAL_SITE_SESSION_SECRETin the Vercel project (production and preview, if previews should authenticate). Rotating it signs everyone out — sealed cookies simply stop opening — which is the intended emergency lever. -
Supabase → Authentication → URL Configuration: add these to the redirect allow-list (project
ytnseohjyvisffnadpib), or every social sign-in and every confirmation link fails at the last hop:https://randal.bot/**https://randal-*-hassion-studio.vercel.app/**— Vercel preview and branch-alias hostshttp://localhost:5174/**— local development
The globstars are load-bearing, not laziness. Every
redirect_tothis site sends carries a query string —/auth/callback?state=…for OAuth,/auth/callback?next=…for confirmation and recovery links — and Supabase matches the whole URL. An exacthttps://randal.bot/auth/callbackentry therefore matches nothing we actually send. In Supabase's matcher the separators are.and/:*stops at them,**crosses them, which is why the host wildcard is*(it spans the hyphenated deployment slug but not.vercel.app) and the path wildcard is**.Leave the existing
randal://,randal-staging://,randal-dev://andhttp://127.0.0.1:7600/onboardingentries alone — those are the mobile apps and the desktop onboarding.The last two entries belong to STAGING, not production (verified 21 Aug: production's list carries only
https://randal.bot/**, the three app schemes and127.0.0.1:7600). Since 21 Aug 2026 Vercel preview deployments and local development authenticate against the staging project (wbihhvdtalfpefseixcx; the site's Preview environment setsPUBLIC_SUPABASE_URL,PUBLIC_SUPABASE_ANON_KEYandRANDAL_PROXY_URL), whose own redirect list carrieshttps://randal-*-hassion-studio.vercel.app/**andhttp://localhost:5174/**. Those two must be removed from the production project — otherwise any preview deployment can mint a production session. Production's final list is the five entries indocs/staging-environment-plan.md§8 ("Auth redirect allow-lists, per project").Symptom when this is wrong: GoTrue silently discards the unlisted
redirect_toand falls back to the project's Site URL, so a preview sign-in lands onhttps://randal.bot/?code=…— production, holding a code it has no verifier for. -
Metering proxy: set the web return URLs so Stripe sends the browser back to the site rather than to the desktop app's
127.0.0.1onboarding:RANDAL_BILLING_WEB_SUCCESS_URL=https://randal.bot/account?checkout=successRANDAL_BILLING_WEB_CANCEL_URL=https://randal.bot/account?checkout=cancel
Both default to exactly those values, so a stock deploy already behaves; set them explicitly when the site lives on another origin.
The flow
/pricing → /signup?plan=pro → /plans?plan=pro → Stripe Checkout → /account?checkout=success
(GoTrue signup) (POST /api/account/checkout, (polls the subscription
platform: "web") until the webhook lands)
The ?checkout=success bounce arrives a beat before Stripe's webhook writes the subscription on the proxy, so /account polls /api/account/summary for up to a minute and says so while it waits. It never claims a plan the ledger has not confirmed.
Deliberate omissions
- No cancellation button. Cancelling issues a prorated refund through the proxy and belongs with the confirmation UI in the app; the site links to Stripe's billing portal instead.
- No card fields anywhere. Stripe's hosted Checkout and billing portal collect payment details. The site never sees a card.
- No account enumeration.
/api/auth/resetalways answers 200, whether or not the address has an account.
Landing Personalization Deployment
The homepage personalization endpoint is intentionally narrow:
- It is a classifier/router only. Model output can select a static preset id, but rich homepage copy, CTAs, quote, FAQ, feature rows, and workflow cards all come from static site-owned preset data.
- The endpoint rejects production requests without an exact allowed
Originheader based onPUBLIC_SITE_URL. - If
TURNSTILE_SECRET_KEYis set, requests must include a valid Cloudflare Turnstile token verified server-side. - In production, if no Turnstile secret is configured, the endpoint fails closed unless
RANDAL_PERSONALIZE_ALLOW_UNPROTECTED=trueis explicitly set. - If no model key is configured, the browser uses deterministic static presets and the site remains useful.
Safe rollout order:
- Deploy the hybrid site with
OPENROUTER_API_KEYunset and verify static preset personalization works. - Set
PUBLIC_SITE_URLto the production origin and redeploy. - Add Turnstile keys and verify challenge-protected requests in preview.
- Add
OPENROUTER_API_KEYonly after the protection path is verified. - If anything fails, remove
OPENROUTER_API_KEY; the site falls back to static presets.
PostHog Setup
PostHog is opt-in. This section covers the public site only; the desktop app, mobile app, and gateway have their own keys and their own privacy rules — see Telemetry for the full picture.
- Create a PostHog project.
- Copy the public project key.
- In Vercel, set
PUBLIC_POSTHOG_ENABLED=true. - Set
PUBLIC_POSTHOG_KEY=phc_.... - Set
PUBLIC_POSTHOG_HOST=https://us.i.posthog.comor the EU ingest host. - Redeploy the site.
If PUBLIC_POSTHOG_ENABLED is absent, not true, or PUBLIC_POSTHOG_KEY is empty, the built site does not include the PostHog snippet.
Domain Setup
- In Vercel, open the site project settings.
- Add the production domain, for example
randal.bot. - Follow Vercel's DNS instructions for your registrar.
- Set
PUBLIC_SITE_URLto the final production origin. - Trigger a production redeploy so canonical URLs use the production domain.
Preview deployments can use Vercel's generated preview URLs. If you need exact canonical preview URLs, set PUBLIC_SITE_URL in the Vercel preview environment.
GitHub Actions Deployment
Do not configure a GitHub Actions Vercel deploy workflow by default. The current deployment path is the Vercel GitHub app, which owns PR previews and production deploys without repo-managed Vercel tokens.
A GitHub Actions deploy could be added later for a specific need, but it should remain an explicit alternative to the Vercel integration, not a second active deploy path for the same site.
Local Verification
Before pushing deployment changes, run:
bun run site:build
bun run test:site
bun run lint
To verify PostHog is disabled by default, build without public PostHog env vars and confirm the generated HTML has no PostHog snippet. To verify opt-in behavior, run a build with PUBLIC_POSTHOG_ENABLED=true and PUBLIC_POSTHOG_KEY=phc_test and confirm the static pages include the public-site snippet.