Architecture
Next.js 15 + Supabase Personal/BC/SM, fronted by Caddy on a DigitalOcean droplet. API routes proxy HubSpot, Microsoft Graph, Basiq, and the LMS. Single-user M365 SSO via Supabase Auth. Live URL: dashboard.finchmax.com.
System shape
Browser (AJ only)
│
▼
Caddy (port 443, auto-SSL)
│
▼
Next.js 15 (port 3000, systemd service)
├── React RSC + Client Components (presentation)
├── TanStack Query (client cache + background refresh)
└── API Routes at /api/* (same Node.js process, no cold starts)
│
├── Supabase Personal (direct client, NEXT_PUBLIC_ keys)
├── Supabase BC (service-role key, server-side only)
├── Supabase SM (service-role key, server-side only)
├── HubSpot CRM Search API (EU1, portal 141955450)
├── Microsoft Graph API (calendar, aj@finchmax.com)
├── Basiq API (Open Banking — CDR + screen scrape)
└── LMS (HMAC-signed iframe URL)
Hosting
DigitalOcean droplet (8GB RAM, 160GB disk, SYD1).
Shared with OpenClaw — Caddy routes by domain.
Dashboard: dashboard.finchmax.com → localhost:3000.
OpenClaw: trinity.q2-group.com.au → localhost:18789.
Auth flow
- User hits dashboard.finchmax.com
- Supabase Auth redirects to Microsoft 365 login (aj@finchmax.com)
- Entra ID enforces MFA (Microsoft Authenticator, 14-day device trust)
- Supabase Auth stores session
- Server-side: session validated on every API route
- Client-side: Supabase client uses NEXT_PUBLIC_ anon key
No app-level auth UI. No registration. Hardcoded email allowlist.
Supabase multi-org strategy
| Org | Entities | Access from dashboard |
|---|---|---|
| Personal | AJ personal (tasks, bank accounts, investments, assets, lists, OKRs) | Direct client (NEXT_PUBLIC_ keys) |
| Bright Connect | BC + Source Energy (events, projections, OKRs) | API routes with service-role key |
| Solar Market | TQC + TCE (events, projections, OKRs) | API routes with service-role key |
Tasks live in Personal org for cross-entity querying. Major events queried real-time from BC/SM (Option A from HLD).
API route map
| Route | Purpose | Integration |
|---|---|---|
| /api/calendar | Calendar read/write proxy | Microsoft Graph |
| /api/banking/balances | On-demand balance fetch | Basiq |
| /api/banking/sync | Cron-triggered daily snapshot | Basiq |
| /api/hubspot/tce-revenue | TCE closed-won revenue by owner | HubSpot CRM Search |
| /api/hubspot/bc-marketing | BC marketing dashboard | HubSpot (needs spec) |
| /api/hubspot/bc-sales | BC sales dashboard | HubSpot (needs spec) |
| /api/hubspot/tqc-leads | TQC lead flow | HubSpot (needs spec) |
| /api/hubspot/tce-leads | TCE lead flow | HubSpot (needs spec) |
| /api/lms/token | HMAC-signed URL for LMS iframe | LMS (Steven Miles) |
| /api/supabase/bc | Proxy to BC Supabase | Supabase service-role |
| /api/supabase/sm | Proxy to SM Supabase | Supabase service-role |
Cron jobs
| Schedule | Command | Purpose |
|---|---|---|
| 0 22 * * * (06:00 AWST) | curl localhost:3000/api/banking/sync?secret=$CRON_SECRET | Daily bank balance snapshot |
Secrets (.env.local on droplet)
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY (Personal), SUPABASE_BC_URL, SUPABASE_BC_SERVICE_ROLE_KEY, SUPABASE_SM_URL, SUPABASE_SM_SERVICE_ROLE_KEY, BASIQ_API_KEY, MS_GRAPH_CLIENT_ID, MS_GRAPH_CLIENT_SECRET, MS_GRAPH_TENANT_ID, HUBSPOT_ACCESS_TOKEN, LMS_AUTH_SECRET, CRON_SECRET.