๐
Permanent Reference
This page is the project brain. It does not change sprint-to-sprint โ only update it when fundamental business or architectural decisions change. For current sprint state, see Current Development.
What Is SONAN DIGITAL?
SONAN DIGITAL is a digital marketing and technology agency. The CRM is an internal operating system built exclusively for SONAN DIGITAL's own use โ not a SaaS product sold to other agencies. It manages the full client lifecycle: leads โ proposals โ contracts โ projects โ invoicing โ support.
Business Objectives
| Objective | Detail |
| Replace fragmented tools | Single platform replaces spreadsheets, separate invoicing tools, and email threads |
| Client self-service portal | Clients view proposals, sign contracts, pay invoices, and submit support tickets without emailing the team |
| Full financial visibility | Track revenue per client, per project, per time period with built-in reporting |
| Operational scalability | Handle growth without proportional increase in admin overhead |
Stack
| Layer | Technology | Notes |
| Frontend / App | Next.js 15 App Router | Edge runtime everywhere โ every route exports export const runtime = 'edge' |
| Database | Supabase (PostgreSQL) | Auth, Storage, RLS, typed client. Two projects: production + UAT |
| Hosting | Vercel | Production on main branch. UAT preview on dev branch at uat.sonandigital.com |
| Email | Resend | 8+ transactional email types. From address uses Resend shared domain or verified custom domain |
| Payments | Stripe | Stripe Checkout, webhooks, auto-paid status on invoice. Test mode for UAT |
| Error tracking | Sentry | Next.js SDK, edge-compatible, captures runtime errors |
| Docs portal | HTML/CSS/JS hosted on Cloudflare Pages | This portal. Repo: sonan-docs. Deployed from GitHub โ Cloudflare Pages |
Architecture
The app has three distinct portals sharing the same codebase and database:
| Portal | Path prefix | Role |
| Admin CRM | /admin/* | super_admin, admin |
| Employee portal | /employee/* | employee |
| Client portal | /portal/* | client |
Data Flow
Server Component (fetch + auth) โ passes props โ Client Component ('use client')
- Server components call Supabase directly (createClient / createServiceClient)
- Client components receive typed data as props, handle UI state
- API routes use requireAdminWithTenant() or getPortalClientId() for auth guards
Multi-tenancy
Every table has tenant_id UUID. Row Level Security (RLS) policies enforce tenant isolation at the database layer. All queries are automatically scoped to the calling user's tenant. getActiveTenantId() retrieves the current tenant from session.
Client Portal Auth
Clients authenticate via Supabase Auth. getPortalClientId(userId) looks up clients.user_id = auth_user_id and returns the client record ID. If this returns null, the user is redirected to /auth/login. Clients must have a clients record with user_id set before they can access portal data pages.
Key Integrations
| Integration | Purpose | Key Detail |
| Supabase Auth | User authentication + TOTP MFA | Email+password + TOTP. Sessions via SSR cookies |
| Supabase Storage | Document file storage | Signed URLs expire after 1 hour (known issue HIGH-6) |
| Stripe Checkout | Invoice payments | Client clicks "Pay Now" โ Stripe Checkout session โ webhook marks invoice paid |
| Resend | Transactional email | Invoice sent, proposal sent, contract signed, support reply, payment confirmation, etc. |
| Sentry | Error tracking | Edge-compatible. DSN set via NEXT_PUBLIC_SENTRY_DSN |
Key Database Tables
| Table | Purpose | Key columns |
clients | Client company records | user_id โ links to auth.users for portal access. assigned_manager_id |
contacts | People within a client company | client_id, is_primary, name, email, phone, title |
proposals | Proposals sent to clients | subtotal_cents (NOT amount_cents), status, public_token |
invoices | Invoices and recurring invoices | subtotal_cents, status, is_recurring, recurrence_interval |
notifications | In-app notifications | user_id NULL = broadcast to all admins. type, resource_type, link |
time_logs | Hours logged by employees | task_id, employee_id, hours, logged_date |
audit_logs | Admin action audit trail | action, resource_type, resource_id, performed_by |
wiki_articles | Internal knowledge base | slug, category, content_html, is_published |
Design Principles
- Edge runtime everywhere โ no Node.js runtime on Vercel; all routes export
runtime = 'edge'
- Server fetches, client renders โ server components fetch data, pass to
'use client' components as props
- Service client for admin ops โ
createServiceClient() bypasses RLS for admin API routes
- Supabase nested joins return arrays โ
.select('*, clients(*)') returns clients as an array even for many-to-one. Always use row.clients?.[0]?.name
- Theme-aware colours only โ no hardcoded hex backgrounds. Use Tailwind semantic classes or CSS variables
- FUSE-safe git commits โ sandbox git operations must use plumbing (hash-object, mktree, commit-tree). Never
git add from the FUSE-mounted path
Business Rules
- A client must have
clients.user_id set before they can log into the portal
- Proposals and invoices use
subtotal_cents โ not amount_cents
- Content approvals use
.action column โ not .decision
- Employees invited via the admin panel must have
role = 'employee' explicitly set (otherwise defaults to 'client')
- All email from-addresses must use the verified Resend domain
- Cron endpoints are protected by
CRON_SECRET in the Authorization header
Environments
Architectural Constraints
- No Node.js APIs โ edge runtime only
- No client-side Supabase service role โ service client is server-only
- All writes to FUSE-mounted git repos must use git plumbing (see CI/CD docs)
- Sandbox (Claude/Cowork) cannot push to GitHub โ user must push from Windows terminal