This guide walks you from an empty machine to a running local dev environment for legalOS, and then through deploying your own instance to Vercel.
Estimated time: 30–45 minutes the first time through.
Before you start, install these:
| Tool | Version | How to check |
|---|---|---|
| Node.js | 20.x or later (LTS recommended) | node --version |
| npm | 10.x or later | npm --version |
| git | any recent version | git --version |
| A code editor | VS Code recommended | — |
Accounts you’ll need:
| Service | Why | Cost |
|---|---|---|
| GitHub | Repo hosting | Free |
| Vercel | App hosting | Free tier is sufficient |
| Supabase | Database + auth | Free tier is sufficient |
| Anthropic | Claude API (needed from Phase 2 onward) | Pay-as-you-go; Phase 0–1 can skip this |
# Create the repo on GitHub first, then:
git clone git@github.com:<your-username>/legalos.git
cd legalos
git clone git@github.com:<your-org>/<your-repo-name>.git
cd <your-repo-name>
# Install dependencies
npm install
# Copy the env template (you'll fill it in next)
cp .env.example .env.local
Leave .env.local with placeholder values for now. The app won’t connect to Supabase or Anthropic yet, but the Next.js dev server will boot.
npm run dev
Open http://localhost:3000. You should see the landing page. Auth and real data won’t work until Supabase is configured in Part 3.
legal-launchpad-dev.
(If you’re setting this up directly for production with no local dev,
name it legal-launchpad-prod instead and follow the production-only
notes in 3f and Part 4.)In the Supabase dashboard, go to Project Settings → API. Copy these three values:
| Value | Where it lives |
|---|---|
| Project URL | NEXT_PUBLIC_SUPABASE_URL |
anon public key |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
service_role secret key |
SUPABASE_SERVICE_ROLE_KEY |
The service_role key bypasses Row-Level Security. Never put it in a client component, never prefix it with NEXT_PUBLIC_, and never commit it.
.env.localNEXT_PUBLIC_SUPABASE_URL=https://<your-project>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your-anon-key>
SUPABASE_SERVICE_ROLE_KEY=<your-service-role-key>
Open the Supabase dashboard → SQL Editor → New query. Paste the contents of supabase/migrations/0001_initial_schema.sql (which is the supabase-schema-v0.sql file from this setup bundle, renamed and placed in the migrations folder) and click Run.
You should see the following tables created:
organizationsusersdepartmentsuser_department_rolesagentsEvery table has RLS enabled and policies in place. Verify this in Authentication → Policies — every table should show at least one policy.
In the Supabase dashboard:
http://localhost:3000 for local dev.http://localhost:3000/**.This is Stage 1 of a two-stage URL Configuration. Stage 2 in Part 4 will add the production Vercel URL once you have one — you can’t fill it in until after the first deploy gives you a URL.
In SQL Editor, run:
-- 1. Create your organization
insert into organizations (id, name, slug)
values (gen_random_uuid(), 'Your Company, Inc.', 'your-company')
returning id;
-- Copy the returned id; you'll paste it into the next queries.
-- 2. Create the eight starting departments (replace ORG_ID with the id from step 1)
insert into departments (organization_id, slug, name, description, sort_order) values
('ORG_ID', 'commercial', 'Commercial', 'Contract review, vendor agreements, commercial operations.', 1),
('ORG_ID', 'public-sector', 'Public Sector', 'Government relations, regulatory affairs, public-sector contracts, and policy advocacy.', 2),
('ORG_ID', 'ma', 'Mergers & Acquisitions', 'Deal diligence, merger agreements, integration planning.', 3),
('ORG_ID', 'privacy', 'Privacy', 'Data privacy, DPAs, regulatory compliance (GDPR, CCPA, etc.).', 4),
('ORG_ID', 'product', 'Product', 'Product launches, feature reviews, terms updates, and product-counsel partnerships.', 5),
('ORG_ID', 'compliance', 'Compliance', 'Compliance program management, regulatory monitoring, and audit support.', 6),
('ORG_ID', 'operations', 'Operations', 'Internal operations, vendor management, procurement, and corporate transactions.', 7),
('ORG_ID', 'general-tools', 'General Tools', 'general purpose agentic tools', 8);
Then create your first admin user. The cleanest way:
npm run dev.http://localhost:3000/login and sign up with email + magic link.org_admin:-- Replace YOUR_EMAIL and ORG_ID
insert into users (id, organization_id, email, role)
select au.id, 'ORG_ID', au.email, 'org_admin'
from auth.users au
where au.email = 'YOUR_EMAIL'
on conflict (id) do update set role = 'org_admin';
-- Grant yourself access to all eight departments
insert into user_department_roles (user_id, department_id, role)
select u.id, d.id, 'dept_admin'
from users u
cross join departments d
where u.email = 'YOUR_EMAIL' and d.organization_id = 'ORG_ID';
Refresh the app. You should now see the admin nav and all eight departments.
If this Supabase project is your production project and you don’t want to spin up local dev just to create the first user, create the auth user directly in the dashboard:
user_department_roles SQL block above —
it joins on auth.users by email, so the user you just created in
step 1 will be picked up.public.users row remains intact; the RPC’s idempotent
guard means first sign-in is a no-op for the admin user. (For
non-admin users created later via magic-link signup, the RPC creates
their public.users row on first sign-in.)This is the path Session 7 used for the production deploy. The dev-signup path above remains the recommended flow for the dev project.
Supabase’s default email provider caps at 2/hour on the free tier, which blocks smoke-testing of magic-link flows. Configuring custom SMTP via Resend lifts this cap. This subsection sets it up in sandbox mode — no verified custom domain — which is enough to validate the rate-limit fix end-to-end with a single recipient (your own Resend account email). Broader-cohort delivery requires a verified custom domain with SPF, DKIM, and DMARC DNS records, deferred pending the subdomain-split decision in D-036.
In sandbox mode the only address that can receive email is the one used to create the Resend account. Sends to any other address fail with a 403, visible in Resend’s email log at resend.com/emails. The 403 is not surfaced in Supabase’s UI, so when delivery seems missing, Resend’s log is the source of truth. There is no “verified recipients” list to manage — the single allowed recipient is fixed at account signup, and broadening it requires the verified-custom-domain path (see the closing note in this subsection).
In the Supabase dashboard, go to Authentication → SMTP Settings (URL: /dashboard/project/_/auth/smtp). Enable the custom-SMTP toggle if there is one, then fill in:
| Field | Value |
|---|---|
| Host | smtp.resend.com |
| Port | 465 |
| Username | resend |
| Password | The Resend API key from the step above |
| Sender email | onboarding@resend.dev |
| Sender name | legalOS |
| Encryption | SSL/TLS (port 465 is SMTPS — immediate TLS, not STARTTLS) |
UI labels in Supabase’s dashboard may differ slightly from the names in this table. Supabase documents the underlying API parameter names but not the UI labels, and the dashboard surface drifts; match by purpose if a label doesn’t exactly correspond.
The point of this subsection is verifying the 2/hour rate-limit cap is gone. Without this step, the SMTP config isn’t proven.
/login (production or a preview deploy) using your Resend account email.resend.com (delivered via Resend), not Supabase’s default sender.Note: Supabase imposes a separate 30/hour rate limit on newly configured custom SMTP servers, adjustable at /dashboard/project/_/auth/rate-limits. That’s plenty of headroom for solo smoke-testing, but worth knowing if you later load-test the magic-link flow.
Two things remain blocked on a future custom-domain decision: (a) broader-cohort delivery — anyone other than the Resend account owner — which requires DNS authentication via SPF, DKIM, and DMARC records on a verified custom domain, and (b) per-environment credential separation, where dev, preview, and prod could share one Resend account or split into separate ones. Both unlock once the subdomain-split question from D-036 is answered.
git add .
git commit -m "chore: initial setup"
git push origin main
In the Vercel project → Settings → Environment Variables, add:
| Variable | Value | Environments |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Your Supabase project URL | Production, Preview, Development |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Your Supabase anon key | Production, Preview, Development |
SUPABASE_SERVICE_ROLE_KEY |
Your Supabase service role key | Production, Preview only (NOT Development) |
ANTHROPIC_API_KEY |
Your Anthropic API key (Phase 2+) | Production, Preview only |
NEXT_PUBLIC_SITE_URL |
Your Vercel production URL (set in Stage 2 — see 4e) | Production only |
Important:
NEXT_PUBLIC_ANTHROPIC_... or NEXT_PUBLIC_SUPABASE_SERVICE_... anywhere, stop and fix it.Chicken-and-egg note on NEXT_PUBLIC_SITE_URL. You can’t set this until Vercel gives you a production URL, which doesn’t exist until the first deploy. So:
NEXT_PUBLIC_SITE_URL during the first import — leave it unset.NEXT_PUBLIC_SITE_URL (then redeploy — env var changes don’t apply until the next build).Preview deploys don’t need a per-branch URL env var. The login server action resolves the magic-link redirect base URL with this fallback chain:
NEXT_PUBLIC_SITE_URL (Production only, after Stage 2)VERCEL_URL — auto-injected by Vercel on every runtime, including every preview deploy. Unique per deploy. Server-only (never NEXT_PUBLIC_).http://localhost:3000 (local dev)You don’t need to set VERCEL_URL anywhere — Vercel injects it automatically. This is what makes preview branches self-test magic-link login without hardcoding URLs. See app/(public)/login/actions.ts for the resolution logic.
After the first deploy (4e) gives you a production URL, come back to Supabase → Authentication → URL Configuration and complete Stage 2:
http://localhost:3000 to your production Vercel URL (e.g., https://your-app.vercel.app).Add to Redirect URLs:
https://your-app.vercel.app/**
https://your-app-*.vercel.app/**
Keep the existing http://localhost:3000/** entry for local dev.
The first pattern covers production. The second covers preview deploys (each preview gets a unique subdomain). Without the wildcard pattern, magic-link clicks from preview deploys will redirect-loop back to login.
Vercel deploys automatically on the first import. If you imported without pushing first, push a small change or click Deploy in the Vercel dashboard.
When the build finishes, copy the production URL Vercel assigns (e.g., https://your-app.vercel.app).
NEXT_PUBLIC_SITE_URL and redeployNEXT_PUBLIC_SITE_URL (Production scope) with the URL from 4e.main, or use Deployments → … → Redeploy in the Vercel dashboard.Once the redeploy finishes, open your Vercel URL. Magic-link login should now redirect to your production domain (not localhost:3000), and you should see the same admin experience as on local dev.
All branding and theme settings live in config/site.ts and config/theme.ts. Department seed data lives in the Supabase departments table (see 3f above).
Edit config/site.ts:
export const siteConfig = {
companyName: "Your Company, Inc.",
siteTitle: "legalOS",
departmentName: "Legal",
themePreset: "carbon", // "carbon" | "modern" | "minimal" | "custom"
adminEmail: "legal-ops@yourcompany.com",
};
The template ships with three presets: Carbon (IBM-inspired, #0f62fe), Modern (indigo, #6366f1), and Minimal (monochrome, #18181b). Pick one in siteConfig.themePreset, or choose "custom" and override the tokens in config/theme.ts.
Departments are database rows, not code. To add a new department:
departments table (see the seed block above for the pattern).To remove a department, soft-delete it by setting is_active = false. Do not hard-delete — existing agents and analytics events reference it.
Once Phase 5’s Agent Admin UI is built, agents can be created and edited in-app by org_admin or dept_admin users. Until then, seed agents directly in SQL:
insert into agents (
organization_id, department_id, slug, name, description, type,
external_url, system_prompt, model, is_active
) values (
'ORG_ID',
(select id from departments where slug = 'commercial' and organization_id = 'ORG_ID'),
'gemini-contract-review',
'Gemini Contract Review',
'Google Gemini Gem for contract review.',
'external',
'https://gemini.google.com/gem/your-gem-id',
null,
null,
true
);
For a native agent, set type = 'native', leave external_url null, and populate system_prompt and model (e.g., 'claude-opus-4-7').
Login works but I can’t see any departments after logging in.
Check the user_department_roles table — your user needs at least one row per department they should see. The admin seed block in 3f grants access to all eight.
“Could not find the function public.xyz” or missing table errors.
The schema migration didn’t run cleanly. Re-run supabase/migrations/0001_initial_schema.sql in the SQL editor. Check the query log for errors.
RLS policy is blocking me from seeing my own data.
This is the policies doing their job, but wrong. Check that your user row exists in public.users (not just auth.users) with the correct organization_id and role. The seed block in 3f handles this.
Vercel build fails on TypeScript errors.
Run npm run build locally first to catch them. TypeScript errors fail the Vercel build on purpose — this is a feature, not a bug.
Anthropic calls return 401.
Your ANTHROPIC_API_KEY is missing, wrong, or not set in Vercel. Check Vercel’s env var settings; remember env var changes require a redeploy to take effect.
Magic link emails aren’t arriving.
Custom SMTP via Resend is configured in 3g. Most likely cause is the sandbox-mode constraint — sends to any address other than the Resend account owner’s email fail with 403, visible at resend.com/emails. If the recipient is correct, check that the Supabase SMTP password still matches an active key at resend.com/api-keys, the custom-SMTP toggle at /dashboard/project/_/auth/smtp is still enabled with fields intact, and the Resend account isn’t paused or over its 3,000/month or 100/day free-tier quota.
Once the app is running, the next work is driven by the phase plan in PROJECT_OUTLINE.md. For a brand-new fork, Phase 0 is complete as soon as this setup guide runs clean end-to-end.
Go read:
PROJECT_OUTLINE.md for the phase roadmap.CLAUDE.md for conventions Claude Code will enforce.DECISION_LOG.md for why the architecture is what it is.