Platform Architecture Plan

Pete (Senior Architect) Β· 22 April 2026 Β· 3-phase roadmap
Architect Pete
Status 5 decisions needed
Stack Next.js + Supabase + Vercel
🎯 The Platform Vision

mojosamurai.com transforms from a marketing brochure into a full business operations platform β€” marketing front, sales pipeline, CRM, and CMS all under one roof and one login. The stack is already aligned: everything runs on Next.js + Supabase + Vercel. No new infrastructure, no new services, no new deployments.

🌐 Public

Marketing site (current). Homepage, services, case studies, about, contact. No changes in Phase 1.

πŸ“‹ Opportunities

Sales pipeline + client briefing docs. Public but unlisted β€” URL is the access control. Phase 2 adds Kanban board in admin.

πŸ” Admin

CRM (contacts, companies, activities), pipeline management, CMS for marketing content. Supabase Auth protected.

πŸ“ What Gets Lifted From Where
FeatureSourceAdaptation
Pipeline Kanban boardPacMer CRM pipelineRemove multi-owner logic. Adapt stage names. Keep card + drag pattern.
Contacts + Companies tablesPacMer CRM schemaSimpler β€” single user, no drip integration, no IMAP sync.
Admin shell + sidebarSonic Compass adminAlready mirrors SC admin pattern. Extend with Pipeline + CRM nav sections.
AdminAuthGuard + requireAdmin()Sonic Compass lib/admin-auth.tsAlready in place. Add permission keys: pipeline, contacts, companies.
Media librarySonic Compass admin/mediaAlready in mojosamurai. Keep as-is.
Activity logNew (not in PacMer yet)Build fresh: contact_id, opportunity_id, type, summary, occurred_at.
🚫
What we deliberately drop

PacMer: IMAP sync, drip engine, campaign blasts, HubSpot refs β€” PacMer-specific. Sonic Compass: brands, products, releases, song deconstructions β€” SC-specific domain. None of this belongs here.

1
File-Based Opportunities Index Done
Hours Β· No database Β· No code changes to add new briefings
  • /opportunities β€” index page reads documents/opportunities/*/meta.json and renders a card grid, grouped by stage.
  • /opportunities/[slug] β€” route handler serves documents/opportunities/[slug]/index.html directly. Noindex, no auth.
  • To add a new opportunity: drop a folder with meta.json + index.html. No code deploy needed.
  • meta.json fields: title, subtitle, stage, created, tags.
⚠️
Decision 4 (see Decisions tab) must be answered before the index page goes to a client

The index lists all opportunity titles and stages. Anyone with that URL sees all deals.

2
Opportunities Pipeline β€” Supabase Kanban Next
1–2 weeks Β· Requires Decisions 1, 2, 3 answered first
  • /admin/pipeline β€” Kanban board with columns per stage. Auth-protected.
  • Each pipeline card stores a slug matching the filesystem briefing.
  • "View Briefing" button on each card links to /opportunities/[slug].
  • A has_briefing badge on the card (green if index.html exists, grey if not).
  • Fields: title, client, company, stage, value estimate, probability, next action, next action due, notes, tags.
Kanban preview (proposed stages):
Identified
Client X
Software platform Β· $80k
Briefing Sent
Incident Reporter
Safety app Β· $60k Β· Briefing βœ“
In Dialogue
Proposal Sent
Won
Lost
3
CRM + Admin 3–4 weeks after Phase 2
Contacts Β· Companies Β· Activity log Β· Marketing CMS
  • /admin/contacts β€” contact list + detail. Warmth signal (cold/warm/hot/customer). Next action + due date.
  • /admin/companies β€” company list, linked to contacts and pipeline opportunities.
  • /admin/activities β€” activity log: call, email, meeting, note, proposal. Linked to contact + opportunity.
  • /admin/content/* β€” existing CMS (pages, blog, case studies, services, media). Already partially built.
  • Pipeline opportunity cards link to a contact record (FK added after Phase 3 migration).
πŸ’‘
Defer from Phase 3

Email integration, campaign blasts, drip sequences, PDF report generation, public contact directory. None of these block the core CRM. Phase 4 territory.

πŸ—ΊοΈ Full Route Architecture
mojosamurai.com/ β”‚ β”œβ”€β”€ (frontend)/ ← Public marketing site (current β€” no changes) β”‚ └── page.tsx, services/, about/, contact/, … β”‚ β”œβ”€β”€ opportunities/ ← Public but noindex β€” URL is the access control β”‚ β”œβ”€β”€ page.tsx ← Index listing all opportunities βœ… Phase 1 Done β”‚ └── [slug]/route.ts ← Serves index.html from filesystem βœ… Phase 1 Done β”‚ β”œβ”€β”€ (admin)/admin/ ← Supabase Auth protected β”‚ β”œβ”€β”€ page.tsx ← Dashboard β”‚ β”‚ β”‚ β”œβ”€β”€ pipeline/ ← Phase 2 β”‚ β”‚ β”œβ”€β”€ page.tsx ← Kanban board β”‚ β”‚ └── [id]/page.tsx ← Deal detail + edit β”‚ β”‚ β”‚ β”œβ”€β”€ content/ ← CMS (existing) β”‚ β”‚ β”œβ”€β”€ pages/, blog/, case-studies/, services/, media/ β”‚ β”‚ β”‚ β”œβ”€β”€ contacts/ ← Phase 3 β”‚ β”‚ β”œβ”€β”€ page.tsx ← Contact list β”‚ β”‚ └── [id]/page.tsx ← Contact detail β”‚ β”‚ β”‚ β”œβ”€β”€ companies/ ← Phase 3 β”‚ β”‚ β”‚ β”œβ”€β”€ activities/ ← Phase 3 β”‚ β”‚ β”‚ └── settings/, users/ β”‚ β”œβ”€β”€ api/admin/ ← Auth-gated API routes β”‚ β”œβ”€β”€ pipeline/route.ts ← Phase 2 β”‚ β”œβ”€β”€ contacts/route.ts ← Phase 3 β”‚ β”œβ”€β”€ companies/route.ts ← Phase 3 β”‚ └── activities/route.ts ← Phase 3 β”‚ └── login/ ← Supabase Auth (existing)
πŸ”‘
Key separation rule

/opportunities/* is never behind auth middleware. It is link-obscured + noindex. The admin pipeline board at /admin/pipeline manages those same records behind full auth. Two different interfaces for two different audiences.

πŸ—„οΈ Phase 2 β€” Pipeline Schema
-- Migration 003: pipeline_opportunities
CREATE TABLE pipeline_opportunities (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  slug            TEXT UNIQUE NOT NULL,          -- matches documents/opportunities/[slug]/
  title           TEXT NOT NULL,
  subtitle        TEXT,
  client_name     TEXT,
  client_company  TEXT,
  stage           TEXT NOT NULL DEFAULT 'identified'
                  CHECK (stage IN ('identified','briefing_sent','in_dialogue',
                                   'proposal_sent','won','lost')),
  value_estimate  INTEGER,                       -- AUD rough estimate
  probability     INTEGER CHECK (probability BETWEEN 0 AND 100),
  briefing_sent_at TIMESTAMPTZ,
  next_action     TEXT,
  next_action_due DATE,
  notes           TEXT,
  tags            TEXT[] DEFAULT '{}',
  created_at      TIMESTAMPTZ DEFAULT now(),
  updated_at      TIMESTAMPTZ DEFAULT now()
);

ALTER TABLE pipeline_opportunities ENABLE ROW LEVEL SECURITY;
CREATE POLICY "pipeline_authenticated" ON pipeline_opportunities
  FOR ALL TO authenticated USING (true) WITH CHECK (true);
πŸ—„οΈ Phase 3 β€” CRM Schema
-- Migration 004: contacts, companies, activities
CREATE TABLE companies (
  id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name       TEXT NOT NULL,
  domain     TEXT UNIQUE,
  industry   TEXT,
  website    TEXT,
  notes      TEXT,
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE contacts (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  display_name    TEXT NOT NULL,
  primary_email   TEXT UNIQUE,
  phone           TEXT,
  title           TEXT,
  company_id      UUID REFERENCES companies(id) ON DELETE SET NULL,
  warmth          TEXT DEFAULT 'cold'
                  CHECK (warmth IN ('cold','warm','hot','customer','dormant')),
  next_action     TEXT,
  next_action_due DATE,
  notes           TEXT,
  tags            TEXT[] DEFAULT '{}',
  created_at      TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE activities (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  contact_id      UUID REFERENCES contacts(id) ON DELETE CASCADE,
  opportunity_id  UUID REFERENCES pipeline_opportunities(id) ON DELETE SET NULL,
  activity_type   TEXT CHECK (activity_type IN
                    ('call','email','meeting','note','proposal')),
  summary         TEXT NOT NULL,
  occurred_at     TIMESTAMPTZ DEFAULT now(),
  created_by      TEXT
);

-- Link contacts to opportunities (added after Phase 3)
ALTER TABLE pipeline_opportunities
  ADD COLUMN contact_id UUID REFERENCES contacts(id) ON DELETE SET NULL;
🚫
Phase 2 cannot start until Decisions 1, 2, and 3 are answered

These have direct schema and UI impact. Wrong answers mean a migration rewrite and a UI rebuild.

Decision 1 β€” Schema blocker
What are the pipeline stage names?
Pete's proposal: Identified β†’ Briefing Sent β†’ In Dialogue β†’ Proposal Sent β†’ Won β†’ Lost

These need to match how you actually think about your sales process. Wrong names = schema migration + UI rebuild later. If you want different words (e.g. "Scoping" instead of "In Dialogue"), say so now.
Blocks Phase 2 Changes the CHECK constraint in the DB schema
Decision 2 β€” UI scope
Drag-and-drop Kanban, or click-to-move?
Drag-and-drop: requires a DnD library (dnd-kit). Feels polished. 2–3 days extra build time.
Click-to-move: "Move to stage" dropdown on each card. Ships in hours. Can add drag later.

Recommendation: click-to-move for Phase 2, drag-and-drop when you've used it for a month and know you want it.
Blocks Phase 2 Different builds β€” can't add drag as an afterthought easily
Decision 3 β€” Storage
Briefings stored in filesystem (repo) or Supabase Storage?
Filesystem (current): HTML files committed to GitHub repo. Deploy to add/update. Simple. Doesn't scale past ~20 briefings.
Supabase Storage: Upload HTML via admin UI. No deploy needed. Correct long-term answer.

Pete's recommendation: keep filesystem for Phase 2. Migrate to Supabase Storage in Phase 3 when you have enough briefings to feel the pain.
Influences Phase 2 Changes the route handler and admin upload UI
Decision 4 β€” Access control
Should the /opportunities index be visible to anyone with the URL?
Right now: anyone who knows www.mojosamurai.com/opportunities sees all deal titles and stages (Minor, Proposal, Won, etc.). If you share a briefing link with Client A and they guess the index URL, they see Client B's deal title.

Options: (a) Accept it β€” unlisted + noindex is fine for now. (b) Put the index behind a simple shared password. (c) Put it behind Supabase Auth (admin only).
Answer before sharing any client links Can be changed later
Decision 5 β€” Users
Is this single-user (Michael only) or will others need access?
The current schema assumes single-user (authenticated = full access to everything). If a VA, business partner, or staff member ever needs pipeline access, every table needs an owner_id column and per-row RLS now β€” not later.

If it's just you: simpler schema, ship faster. Confirm and Pete will finalise the migration.
Answer before Phase 2 schema Adding owner_id later = data migration