๐Ÿ“Œ
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

ObjectiveDetail
Replace fragmented toolsSingle platform replaces spreadsheets, separate invoicing tools, and email threads
Client self-service portalClients view proposals, sign contracts, pay invoices, and submit support tickets without emailing the team
Full financial visibilityTrack revenue per client, per project, per time period with built-in reporting
Operational scalabilityHandle growth without proportional increase in admin overhead

Stack

LayerTechnologyNotes
Frontend / AppNext.js 15 App RouterEdge runtime everywhere โ€” every route exports export const runtime = 'edge'
DatabaseSupabase (PostgreSQL)Auth, Storage, RLS, typed client. Two projects: production + UAT
HostingVercelProduction on main branch. UAT preview on dev branch at uat.sonandigital.com
EmailResend8+ transactional email types. From address uses Resend shared domain or verified custom domain
PaymentsStripeStripe Checkout, webhooks, auto-paid status on invoice. Test mode for UAT
Error trackingSentryNext.js SDK, edge-compatible, captures runtime errors
Docs portalHTML/CSS/JS hosted on Cloudflare PagesThis portal. Repo: sonan-docs. Deployed from GitHub โ†’ Cloudflare Pages

Architecture

The app has three distinct portals sharing the same codebase and database:

PortalPath prefixRole
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

IntegrationPurposeKey Detail
Supabase AuthUser authentication + TOTP MFAEmail+password + TOTP. Sessions via SSR cookies
Supabase StorageDocument file storageSigned URLs expire after 1 hour (known issue HIGH-6)
Stripe CheckoutInvoice paymentsClient clicks "Pay Now" โ†’ Stripe Checkout session โ†’ webhook marks invoice paid
ResendTransactional emailInvoice sent, proposal sent, contract signed, support reply, payment confirmation, etc.
SentryError trackingEdge-compatible. DSN set via NEXT_PUBLIC_SENTRY_DSN

Key Database Tables

TablePurposeKey columns
clientsClient company recordsuser_id โ€” links to auth.users for portal access. assigned_manager_id
contactsPeople within a client companyclient_id, is_primary, name, email, phone, title
proposalsProposals sent to clientssubtotal_cents (NOT amount_cents), status, public_token
invoicesInvoices and recurring invoicessubtotal_cents, status, is_recurring, recurrence_interval
notificationsIn-app notificationsuser_id NULL = broadcast to all admins. type, resource_type, link
time_logsHours logged by employeestask_id, employee_id, hours, logged_date
audit_logsAdmin action audit trailaction, resource_type, resource_id, performed_by
wiki_articlesInternal knowledge baseslug, 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

EnvironmentURLBranchDatabase
Productionsonandigital.commainProduction Supabase project
UAT / Previewuat.sonandigital.comdevUAT Supabase project (separate DB)

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