Docs

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

ServiceWhatWhere
apps/landingNext.js — marketing site (static, no Supabase)Vercel
apps/authNext.js — signin/signup/claim/forgotVercel
apps/roomsNext.js — live rooms + dashboardVercel
apps/homeNext.js — workspace hub / launcherVercel
apps/pagesNext.js — collaborative documentsVercel
apps/chatNext.js — workspace chatVercel
apps/apiFastAPI + 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 push auto-deploys each Vercel project (git-connected, Root Directory apps/landing / apps/auth / apps/rooms / apps/home / apps/pages / apps/chat respectively — six separate Vercel projects, one repo). Or cd apps/<name> && npx vercel --prod for a manual deploy of just one.
  • API — pushing to main with changes under apps/api runs the Deploy API to EC2 GitHub Action: it SSHes in, rebuilds the Docker image, applies any new supabase/migrations with scripts/apply_migrations.py, then restarts the container. Production secrets live in apps/api/.env on 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 needs BAAI/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.com

Rooms 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.com

Home / 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)

  1. 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 apex meet-align.com moves to the new Landing project (align-landing) — it used to sit on the Rooms project, back when Rooms served the marketing site too.
  2. Set each project's env vars above (Production), redeploy.
  3. 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 from meet-align.com to auth.meet-align.com — it's now the app that actually owns the auth flow, and any unmatched redirect lands there instead.
  4. API → CORS_ORIGIN must list every host in one secrets set call (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
  5. Auth owns /signin, /signup, /claim, /forgot, /auth/callback, /auth/reset — the only app with a sign-in UI. Rooms' proxy.ts gates /dashboard/* and bounces signed-out visitors to auth.meet-align.com/signin?next=<original url>; /start, /room/[code], /history, /recorder stay open to anonymous visitors. Home/Pages/Chat have no sign-in UI of their own either — each one's proxy.ts bounces 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. 0004 adds profiles.welcomed_at; 0005 adds teams / team_members / team_invites, extends the signup trigger to create a personal team, and backfills existing accounts. Apply 0005 in 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.com or https://auth.meet-align.com; Redirect URLs needs every app host's /**.
  • Custom SMTP (Titan): smtp.titan.email:587, hello@meet-align.com, sender hello@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.

  1. Apply supabase/migrations/0004_welcome_email.sql (adds profiles.welcomed_at).

  2. Set these API secrets: SMTP_USER, SMTP_PASS, WELCOME_HOOK_SECRET (openssl rand -hex 24).

  3. Wire the trigger. The Supabase "Database Webhooks" UI needs the supabase_functions schema, which isn't provisioned on this project ("schema supabase_functions does not exist"), so run the equivalent pg_net trigger by hand in the SQL Editor — it is not committed because it carries the WELCOME_HOOK_SECRET literal 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();
  4. Test: create a throwaway account → the mail arrives, and profiles.welcomed_at is 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 on https://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 in apps/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-set teams.plan column until billing lands. Anonymous rooms stay text-only. Set FREE_TIER_SESSION_LIMIT=0 to 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_* (see apps/api/.env.example) — per-IP room/upload/socket caps.

Last updated on

On this page