Inroads
A personal relationship and networking tool for people whose work depends on who they know. It's built around three ideas: goals first, rooms (events) as first-class, and a two-way promises ledger. Lagos is the first market.
Stack
Next.js 16 (App Router, Cache Components) · TypeScript strict · Tailwind CSS v4 · Supabase (Postgres, Auth) · Drizzle ORM · Zod · Vitest · Playwright. Inngest for background jobs (optional) and the Anthropic API for AI features. Hosted on Vercel's free tier. Email (Resend) arrives in P2, with the daily brief by email.
Run it locally
You need Node 22, pnpm 10, and a Supabase project (the free plan is fine).
pnpm install
cp .env.example .env.local # then fill it in, see below
pnpm db:migrate # applies supabase/migrations to DATABASE_URL
pnpm dev # http://localhost:3000
Sign in with Google, then go to Settings → Load example data to see the app with a small Lagos network in it.
One-time Supabase setup
- Create a project at supabase.com and pick the London (eu-west-2) region.
- Google sign-in.
- In Google Cloud Console → APIs & Services → Credentials, create an OAuth client ID of type Web application.
- Authorised redirect URI:
https://<project-ref>.supabase.co/auth/v1/callback.
- Paste the client ID and secret into Supabase → Authentication → Sign In / Providers → Google.
- This sign-in only asks for name and email. Contacts and Calendar access is a separate step (milestone 5).
- Redirect URLs. In Supabase → Authentication → URL Configuration:
- Set Site URL to your deployed URL, e.g.
https://inroads.vercel.app.
- Add these to Redirect URLs:
http://localhost:3000/** and https://inroads.vercel.app/**.
- Keys and database.
- Project URL and anon (publishable) key: Supabase → Project Settings → API.
- Database URL: Connect → Transaction pooler (port 6543) for the app.
- Run migrations against the Session pooler (port 5432).
Email magic links are built but switched off (NEXT_PUBLIC_MAGIC_LINK_ENABLED=false) until there's a domain to send email from. See docs/TECH_PLAN.md §12.
Environment variables
| Name | Needed for | Notes |
|---|
NEXT_PUBLIC_SUPABASE_URL | everything | https://<ref>.supabase.co. Inlined at build time. |
NEXT_PUBLIC_SUPABASE_ANON_KEY | everything | Anon / publishable key. Safe in the browser; RLS protects data. |
DATABASE_URL | server | Postgres connection string. The app connects as the table owner and drops to the authenticated role per request, so RLS applies (see src/server/db/with-user.ts). |
SUPABASE_SERVICE_ROLE_KEY | deleting accounts | Server only. Never expose it. Used to delete the sign-in through the Supabase Admin API (which also ends sessions). Without it the auth row is deleted directly in Postgres. |
APP_URL | server | Public base URL. |
NEXT_PUBLIC_MAGIC_LINK_ENABLED | optional | true once a sending domain is set up. |
TOKEN_ENC_KEYS | connections | JSON map of key id to a base64 32-byte key, e.g. {"v1":"…"}. Encrypts OAuth tokens and calendar links (AES-256-GCM). Generate one with node -e "console.log(require('crypto').randomBytes(32).toString('base64'))". |
TOKEN_ENC_CURRENT | connections | The key id new secrets are encrypted with (v1). To rotate, add v2 to the map, set this to v2, and keep v1 until old rows have re-synced. |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | Google connection | See Connect Google below. Without them the Google card says it isn't set up. |
MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET | Outlook connection | See Connect Outlook below. MICROSOFT_TENANT is optional (default common). |
INNGEST_EVENT_KEY, INNGEST_SIGNING_KEY | background sync | From Inngest (free tier). Without them a sync runs right after the request that started it, and there are no scheduled re-syncs. |
|
RESEND_API_KEY arrives with the emailed daily brief (P2).
Connect Google (Contacts and Calendar)
- In Google Cloud Console, create a project (or reuse the one used for sign-in).
- APIs & Services → Library: enable the People API and the Google Calendar API.
- OAuth consent screen: User type External, publishing status Testing. Add the scopes
contacts.readonly, calendar.events.readonly and calendar.calendarlist.readonly. Under Test users, add the Google accounts that will use it (up to 100).
- Credentials → Create credentials → OAuth client ID → Web application. Authorised redirect URIs:
http://localhost:3000/api/oauth/google/callback
https://inroads.vercel.app/api/oauth/google/callback
- Put the client id and secret in
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET.
While the app is in Testing, Google expires the connection after 7 days; Inroads then shows "Needs reconnecting". Publishing the app (and Google's verification for these scopes) needs a domain with a privacy policy; see docs/TECH_PLAN.md §12.
Connect Outlook (Microsoft 365 and Outlook.com)
- In the Azure portal → Microsoft Entra ID → App registrations → New registration. Supported account types: Accounts in any organizational directory and personal Microsoft accounts.
- Redirect URI (Web):
https://inroads.vercel.app/api/oauth/microsoft/callback (add the localhost one too).
- API permissions → Microsoft Graph → Delegated:
User.Read, Contacts.Read, Calendars.Read, offline_access.
- Certificates & secrets → New client secret. Put the Application (client) ID and the secret value in
MICROSOFT_CLIENT_ID / MICROSOFT_CLIENT_SECRET.
Background jobs (Inngest)
- Create a free account at inngest.com and an app.
- Copy the Event key and Signing key into
INNGEST_EVENT_KEY / INNGEST_SIGNING_KEY on Vercel.
- After deploying, sync the app in Inngest with the URL
https://inroads.vercel.app/api/inngest. That registers the sync, the 30-minute re-sync schedule and the daily clean-up.
Locally, npx inngest-cli@latest dev runs the dev server; or leave the keys unset and syncs run inline.
Calendar links (Apple iCloud, Outlook "publish", any .ics) and the Android Contact Picker need no setup.
Tests
pnpm typecheck
pnpm lint
pnpm test # unit tests + database tests (RLS, Space sealing, triggers, example data)
pnpm build && pnpm test:e2e # Playwright on desktop and mobile viewports, with axe accessibility checks
- Database tests run against plain Postgres.
supabase/tests/supabase-stub.sql stands in for the parts of Supabase the migrations use: the auth schema, auth.uid() and the roles.
- End-to-end tests sign in through a small fake auth server (
tests/e2e/support/fake-supabase.ts) that signs real ES256 tokens, so the app's real token verification runs unchanged. The same server plays Google (OAuth, People, Calendar) via TEST_FAKE_PROVIDERS_URL, so the connect → sync → sort flow runs end to end. The build must use NEXT_PUBLIC_SUPABASE_URL=http://localhost:54399 and NEXT_PUBLIC_SUPABASE_ANON_KEY=e2e-anon-key (CI does this).
Database changes
Edit src/server/db/schema.ts, then run pnpm db:generate. Triggers, functions and grants go in a custom migration (pnpm drizzle-kit generate --custom --name=...). Every new user-owned table needs a user_id, the owner policies, and an entry in tests/db/rls.test.ts.
Deploy (Vercel, free tier)
- Import the GitHub repo in Vercel. It detects Next.js. Functions run in London (
vercel.json).
- Add the environment variables above under Project → Settings → Environment Variables.
- Deploy. Before the first deploy, and after any schema change, run
DATABASE_URL=<session pooler URL> pnpm db:migrate.
- Put the deployed URL into Supabase's Site URL and Redirect URLs.
Vercel's free plan is for non-commercial use. Move to Pro before charging users. Supabase's free plan pauses a project after about a week with no activity; you can resume it from the dashboard.