Deployment
How Align's services (Vercel apps + the API on AWS EC2) are deployed, configured, and wired together.
Live: marketing https://meet-align.com (Vercel align-landing) · Auth
https://auth.meet-align.com (Vercel align-auth) · Rooms
https://rooms.meet-align.com (Vercel align-rooms) · Home
https://home.meet-align.com (Vercel align-home) · Pages
https://pages.meet-align.com (Vercel align-docs — renamed from "Docs" to
free up that name for the real developer-docs site) · Chat
https://chat.meet-align.com (Vercel align-chat) · API
https://api.meet-align.com (AWS EC2, Docker Compose).
| Service | What | Where |
|---|---|---|
apps/landing | Next.js — marketing site (static, no Supabase) | Vercel |
apps/auth | Next.js — signin/signup/claim/forgot | Vercel |
apps/rooms | Next.js — live rooms + dashboard | Vercel |
apps/home | Next.js — workspace hub / launcher | Vercel |
apps/pages | Next.js — collaborative documents | Vercel |
apps/chat | Next.js — workspace chat | Vercel |
apps/api | FastAPI + Socket.IO (live rooms + voice bot) | EC2 |
There is no work/admin app yet — both render disabled in every app's nav
until they exist.
The API cannot run on Vercel: one long-lived process, in-memory room state,
refuses to start with WEB_CONCURRENCY != 1 (the API's main.py). Scaling
past one instance needs the Redis-adapter work — not built yet.
Managed deps: Supabase (Postgres + Auth) · Groq (LLM) · LiveKit Cloud (video relay) · Deepgram (STT + Aura TTS). All on a free tier / credit.
Shipping changes
- Landing, Auth, Rooms, Home, Pages, Chat —
git pushauto-deploys each Vercel project (git-connected, Root Directoryapps/landing/apps/auth/apps/rooms/apps/home/apps/pages/apps/chatrespectively — six separate Vercel projects, one repo). Orcd apps/<name> && npx vercel --prodfor a manual deploy of just one. - API — pushing to
mainwith changes underapps/apiruns theDeploy API to EC2GitHub Action: it SSHes in, rebuilds the Docker image, applies any newsupabase/migrationswithscripts/apply_migrations.py, then restarts the container. Production secrets live inapps/api/.envon the box.
API config
VOICE=1(Docker build arg) — the image bundles the Pipecat voice bot.VOICE_MAX_CONCURRENT— AI-voice rooms at once (default 1; others still get video, align just stays in chat).EMBED_MODEL— empty means keyword retrieval (semantic needsBAAI/bge-small-en-v1.5+ ~1 GB more RAM).
Secrets (apps/api/.env on the EC2 box)
LLM_API_KEY (Groq) · DATABASE_URL (Supabase transaction pooler, port 6543,
@→%40, ?sslmode=require) · SUPABASE_URL · SUPABASE_SERVICE_ROLE_KEY ·
CORS_ORIGIN (comma-separated origins) · LIVEKIT_URL/API_KEY/API_SECRET ·
DEEPGRAM_API_KEY · VOICE_BOT_SECRET. Add new ones to that file and redeploy. JWT verification uses
the project JWKS — no SUPABASE_JWT_SECRET needed.
Founder welcome email adds SMTP_USER (hello@meet-align.com) · SMTP_PASS ·
WELCOME_HOOK_SECRET (random). Optional: EMAIL_FROM, FOUNDER_NAME,
SMTP_HOST/SMTP_PORT (default GoDaddy smtpout.secureserver.net:465),
PUBLIC_APP_URL.
Meeting recording (LiveKit Egress → Supabase Storage) adds
SUPABASE_S3_ACCESS_KEY · SUPABASE_S3_SECRET_KEY · SUPABASE_S3_REGION
(project region). Create a private recordings bucket and an S3 access key
in Supabase → Storage → S3 Connection. Missing ⇒ transcript-only, no recording.
Landing env (Vercel project align-landing)
Static marketing site, no Supabase/API dependency at all:
NEXT_PUBLIC_ROOMS_APP_URL = https://rooms.meet-align.com
NEXT_PUBLIC_AUTH_APP_URL = https://auth.meet-align.com
NEXT_PUBLIC_PAGES_APP_URL = https://pages.meet-align.com
NEXT_PUBLIC_CHAT_APP_URL = https://chat.meet-align.com
NEXT_PUBLIC_POSTHOG_HOST = https://us.i.posthog.com
NEXT_PUBLIC_POSTHOG_KEY = <optional>Auth env (Vercel project align-auth)
NEXT_PUBLIC_API_URL = https://api.meet-align.com
NEXT_PUBLIC_APP_URL = https://auth.meet-align.com
NEXT_PUBLIC_SUPABASE_URL = https://<ref>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY = <anon key>
NEXT_PUBLIC_COOKIE_DOMAIN = .meet-align.com # note the leading dot — not optional
NEXT_PUBLIC_LANDING_APP_URL = https://meet-align.com
NEXT_PUBLIC_ROOMS_APP_URL = https://rooms.meet-align.comRooms env (Vercel project align-rooms — Production)
NEXT_PUBLIC_API_URL = https://api.meet-align.com
NEXT_PUBLIC_APP_URL = https://rooms.meet-align.com
NEXT_PUBLIC_SUPABASE_URL = https://<ref>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY = <anon key>
NEXT_PUBLIC_POSTHOG_HOST = https://us.i.posthog.com
NEXT_PUBLIC_POSTHOG_KEY = <optional>
NEXT_PUBLIC_COOKIE_DOMAIN = .meet-align.com # note the leading dot
NEXT_PUBLIC_LANDING_APP_URL = https://meet-align.com
NEXT_PUBLIC_AUTH_APP_URL = https://auth.meet-align.com
NEXT_PUBLIC_HOME_APP_URL = https://home.meet-align.com
NEXT_PUBLIC_ROOMS_APP_URL = https://rooms.meet-align.com
NEXT_PUBLIC_PAGES_APP_URL = https://pages.meet-align.com
NEXT_PUBLIC_CHAT_APP_URL = https://chat.meet-align.comHome / Pages / Chat env (Vercel projects align-home / align-docs / align-chat)
Same shape as each app's .env.example — NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_APP_URL (that app's own origin), NEXT_PUBLIC_AUTH_APP_URL
(https://auth.meet-align.com — the only app with a sign-in UI),
NEXT_PUBLIC_LANDING_APP_URL (where a signed-out visit with no marketing
page of its own lands), NEXT_PUBLIC_HOME_APP_URL /
NEXT_PUBLIC_ROOMS_APP_URL / NEXT_PUBLIC_PAGES_APP_URL /
NEXT_PUBLIC_CHAT_APP_URL, the same Supabase pair, and
NEXT_PUBLIC_COOKIE_DOMAIN = .meet-align.com — this one is not
optional: without it the shared session cookie is invisible to that
subdomain and its proxy.ts auth gate loops forever.
Subdomain setup (meet-align.com marketing + 6 app subdomains)
- Each app is its own Vercel project (Root Directory
apps/<name>) with its own domain:auth.meet-align.com,rooms.meet-align.com,home.meet-align.com,pages.meet-align.com,chat.meet-align.com— nameservers are on Vercel so each DNS record is created automatically when added under that project's Settings → Domains. The apexmeet-align.commoves to the new Landing project (align-landing) — it used to sit on the Rooms project, back when Rooms served the marketing site too. - Set each project's env vars above (Production), redeploy.
- Supabase → Auth → URL Configuration → Redirect URLs → add
https://auth.meet-align.com/**,https://rooms.meet-align.com/**,https://home.meet-align.com/**,https://pages.meet-align.com/**,https://chat.meet-align.com/**. Also decide whether Site URL should move frommeet-align.comtoauth.meet-align.com— it's now the app that actually owns the auth flow, and any unmatched redirect lands there instead. - API →
CORS_ORIGINmust list every host in onesecrets setcall (it replaces the value, it doesn't append):CORS_ORIGIN="https://meet-align.com,https://auth.meet-align.com,https://rooms.meet-align.com,https://home.meet-align.com,https://pages.meet-align.com,https://chat.meet-align.com"in the API.env - Auth owns
/signin,/signup,/claim,/forgot,/auth/callback,/auth/reset— the only app with a sign-in UI. Rooms'proxy.tsgates/dashboard/*and bounces signed-out visitors toauth.meet-align.com/signin?next=<original url>;/start,/room/[code],/history,/recorderstay open to anonymous visitors. Home/Pages/Chat have no sign-in UI of their own either — each one'sproxy.tsbounces the same way. The session cookie is scoped to.meet-align.com, so signing in on Auth works on every host, and the callback sends dashboard-bound visitors back to Rooms.
Supabase
- Migrations:
supabase/migrations/000{1..5}.sql.0004addsprofiles.welcomed_at;0005addsteams/team_members/team_invites, extends the signup trigger to create a personal team, and backfills existing accounts. Apply0005in the SQL editor before the Team page goes live. Team invite emails reuse the SMTP config below — no extra setup. - Auth → URL Configuration → Site URL + Redirect URLs. See the subdomain-setup
decision above on whether Site URL should be
https://meet-align.comorhttps://auth.meet-align.com; Redirect URLs needs every app host's/**. - Custom SMTP (Titan):
smtp.titan.email:587,hello@meet-align.com, senderhello@meet-align.com. Add SPF + DKIM DNS records or mail lands in spam.
Founder welcome email
One personalised email from the founder to every new account.
-
Apply
supabase/migrations/0004_welcome_email.sql(addsprofiles.welcomed_at). -
Set these API secrets:
SMTP_USER,SMTP_PASS,WELCOME_HOOK_SECRET(openssl rand -hex 24). -
Wire the trigger. The Supabase "Database Webhooks" UI needs the
supabase_functionsschema, which isn't provisioned on this project ("schema supabase_functions does not exist"), so run the equivalentpg_nettrigger by hand in the SQL Editor — it is not committed because it carries theWELCOME_HOOK_SECRETliteral and the repo is public:create extension if not exists pg_net; create or replace function public.tg_welcome_email() returns trigger language plpgsql security definer set search_path = '' as $$ begin perform net.http_post( url := 'https://api.meet-align.com/api/hooks/welcome', headers := jsonb_build_object( 'Content-Type', 'application/json', 'x-webhook-secret', '<WELCOME_HOOK_SECRET>'), body := jsonb_build_object('record', to_jsonb(new))); return new; end $$; drop trigger if exists welcome_email on public.profiles; create trigger welcome_email after insert on public.profiles for each row execute function public.tg_welcome_email(); -
Test: create a throwaway account → the mail arrives, and
profiles.welcomed_atis set. The endpoint claims the row before sending, so a retried delivery never double-sends.
Smoke test
https://meet-align.com/loads (static marketing, no console errors).https://auth.meet-align.com/signin→ Google sign-in → lands onhttps://rooms.meet-align.com/dashboard.- New session → room opens, moderator kickoff arrives.
- Same room code in a 2nd browser → both see each other (proves the socket fan-out).
- Video meeting + a doc + Start → video tiles + align bot, responds to speech.
Limits
- Plans (table in
apps/api/hudl/billing/plans.py, env inapps/api/.env.example) — only AI Rooms are metered; Meet and Async are unlimited. Free: 3 voice AI Rooms per calendar month (≤ 10 min each), records visible 90 days, 5 members. Team: 120 AI Room minutes per person per month, pooled across the team, 60-min rooms. Business: 300 min per person, 4-hour rooms, private decisions. Enterprise: unmetered. A plan is the hand-setteams.plancolumn until billing lands. Anonymous rooms stay text-only. SetFREE_TIER_SESSION_LIMIT=0to switch every quota off. - One API instance — small beta only; single point of failure; no horizontal scale yet.
- Local JSON archive (
data/sessions/) is ephemeral on the container (Postgres is the source of truth for the dashboard). RATE_LIMIT_*(seeapps/api/.env.example) — per-IP room/upload/socket caps.
Last updated on
