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.
Marketing site (current). Homepage, services, case studies, about, contact. No changes in Phase 1.
Sales pipeline + client briefing docs. Public but unlisted β URL is the access control. Phase 2 adds Kanban board in admin.
CRM (contacts, companies, activities), pipeline management, CMS for marketing content. Supabase Auth protected.
| Feature | Source | Adaptation |
|---|---|---|
| Pipeline Kanban board | PacMer CRM pipeline | Remove multi-owner logic. Adapt stage names. Keep card + drag pattern. |
| Contacts + Companies tables | PacMer CRM schema | Simpler β single user, no drip integration, no IMAP sync. |
| Admin shell + sidebar | Sonic Compass admin | Already mirrors SC admin pattern. Extend with Pipeline + CRM nav sections. |
| AdminAuthGuard + requireAdmin() | Sonic Compass lib/admin-auth.ts | Already in place. Add permission keys: pipeline, contacts, companies. |
| Media library | Sonic Compass admin/media | Already in mojosamurai. Keep as-is. |
| Activity log | New (not in PacMer yet) | Build fresh: contact_id, opportunity_id, type, summary, occurred_at. |
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.
- /opportunities β index page reads
documents/opportunities/*/meta.jsonand renders a card grid, grouped by stage. - /opportunities/[slug] β route handler serves
documents/opportunities/[slug]/index.htmldirectly. 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.
The index lists all opportunity titles and stages. Anyone with that URL sees all deals.
- /admin/pipeline β Kanban board with columns per stage. Auth-protected.
- Each pipeline card stores a
slugmatching the filesystem briefing. - "View Briefing" button on each card links to
/opportunities/[slug]. - A has_briefing badge on the card (green if
index.htmlexists, grey if not). - Fields: title, client, company, stage, value estimate, probability, next action, next action due, notes, tags.
- /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).
Email integration, campaign blasts, drip sequences, PDF report generation, public contact directory. None of these block the core CRM. Phase 4 territory.
/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.
-- 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);
-- 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;
These have direct schema and UI impact. Wrong answers mean a migration rewrite and a UI rebuild.
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.
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.
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.
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).
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.