Content Review Portal — Build Spec

Replace the Google Docs review workflow with a built-in review page on stylify-ai.com
Prepared by: Charlotte (COO) · February 24, 2026
For: Stitch (CTO) — Build Assignment
Priority: Medium-High · Estimated Effort: 5–8 hours
Depends on: Nothing (can be built during production freeze — internal tool only)

1. Why We're Building This

Our current content review workflow uses Google Docs in a shared Drive folder. It has too much friction:

The review portal solves all of this by putting the review workflow inside our own infrastructure where all three participants have clean, purpose-built interfaces.

Key Principle

Kristi's experience should be: open a link on her phone, read content, tap to approve or leave a note. That's it. No folder management. No mode switching. No learning curve.

2. User Roles & Access

Three users interact with this system. Each has a different access pattern.

User Access Method What They Do
Charlotte (COO agent) REST API endpoints with admin JWT, or browser UI Submit content for review, read Kristi's feedback, update content after changes
Kristi (Reviewer) Magic link (unique token URL, no password) View pending items, approve/flag content, leave inline comments
Jason (Founder) Admin dashboard (existing admin auth) See review status dashboard, override decisions if needed

2a. Kristi's Magic Link Auth

Kristi is not a Stylify user — she doesn't have an account and shouldn't need one. Instead:

  1. A reviewers table stores her email and a unique access token (UUID v4).
  2. Her bookmark URL looks like: stylify-ai.com/review?token=abc123...
  3. The token is long-lived (no expiry unless manually rotated) and grants access to the review interface only.
  4. If the token is compromised, Jason regenerates it from the admin panel. The old URL stops working.
  5. The token only grants read/comment/approve access to content_reviews. No other API access.
Why Not a Password?

Kristi reviews on her phone. A magic link she bookmarks once is zero-friction. Adding a password adds a login screen, a "forgot password" flow, and a barrier every time she opens the link. For an internal reviewer with one user, a long-lived token URL is the right tradeoff.

3. Database Schema

Two new tables. Follows the existing pattern: BIGINT auto-incrementing IDs, timestamps, foreign keys to users table where applicable.

3a. content_reviews — The Review Items

-- Migration: 022_content_review_portal.sql
-- Run one statement at a time in Supabase SQL Editor

CREATE TABLE content_reviews (
  id            BIGSERIAL PRIMARY KEY,
  title         TEXT NOT NULL,           -- e.g. "Email 1: Welcome + Playbook"
  content_type  TEXT NOT NULL,           -- 'email' | 'landing_page' | 'scorecard' | 'lead_magnet' | 'instagram' | 'dm_template' | 'legal' | 'other'
  content_body  TEXT NOT NULL,           -- The full content (plain text or markdown)
  context_brief TEXT,                    -- "What is this? Why does it matter?" for the reviewer
  status        TEXT NOT NULL DEFAULT 'pending',  -- 'pending' | 'approved' | 'needs_changes' | 'revised' | 'archived'
  version       INTEGER NOT NULL DEFAULT 1,     -- Increments on each revision
  parent_id     BIGINT REFERENCES content_reviews(id),  -- Links revised versions to original
  submitted_by  TEXT NOT NULL DEFAULT 'charlotte',   -- Which agent submitted this
  expert_score  NUMERIC(3,1),            -- Expert Panel average score (e.g. 7.8)
  brand_rules   TEXT,                    -- Relevant brand rules shown to reviewer
  live_url      TEXT,                    -- Link to see it live (if applicable)
  reviewed_by   TEXT,                    -- Reviewer name (filled on approval/rejection)
  reviewed_at   TIMESTAMPTZ,             -- When the decision was made
  created_at    TIMESTAMPTZ DEFAULT NOW(),
  updated_at    TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_content_reviews_status ON content_reviews(status);
CREATE INDEX idx_content_reviews_type ON content_reviews(content_type);

3b. review_comments — Inline Feedback

CREATE TABLE review_comments (
  id          BIGSERIAL PRIMARY KEY,
  review_id   BIGINT NOT NULL REFERENCES content_reviews(id) ON DELETE CASCADE,
  author      TEXT NOT NULL,             -- 'kristi' | 'jason' | 'charlotte'
  comment     TEXT NOT NULL,             -- The feedback text
  section_ref TEXT,                      -- Which section this comment is about (e.g. "Q3" or "CTA headline")
  original_text TEXT,                   -- The specific text being commented on (for inline edits)
  suggested_text TEXT,                  -- The replacement text (if suggesting a rewrite)
  resolved    BOOLEAN DEFAULT FALSE,     -- Charlotte marks resolved after incorporating
  created_at  TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_review_comments_review ON review_comments(review_id);

3c. reviewers — Reviewer Access Tokens

CREATE TABLE reviewers (
  id        BIGSERIAL PRIMARY KEY,
  name      TEXT NOT NULL,           -- 'Kristi'
  email     TEXT UNIQUE NOT NULL,     -- 'kristiwait@hotmail.com'
  token     TEXT UNIQUE NOT NULL,     -- UUID v4, used in URL
  is_active BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Seed Kristi's reviewer record
INSERT INTO reviewers (name, email, token)
VALUES ('Kristi', 'kristiwait@hotmail.com', gen_random_uuid()::text);
Re-Approval Rule Built In

When approved content is edited, Charlotte creates a new content_reviews row with parent_id pointing to the original and version incremented. The original's status changes to 'revised'. Kristi sees the new version in her queue automatically. The parent_id chain gives us full version history.

4. API Endpoints

New route file: backend/src/routes/reviewRoutes.js

4a. Reviewer-Facing (Token Auth)

These endpoints use the reviewer token from the URL query parameter. Middleware: authenticateReviewer — looks up token in reviewers table, attaches reviewer info to req.reviewer.

MethodEndpointWhat It Does
GET /api/reviews?token=xxx List all reviews. Default: pending first, then needs_changes, then approved. Returns: id, title, content_type, status, version, expert_score, created_at, comment_count.
GET /api/reviews/:id?token=xxx Full review detail: all fields from content_reviews + all comments for this review.
POST /api/reviews/:id/approve?token=xxx Sets status='approved', reviewed_by=reviewer name, reviewed_at=now(). Body: optional { note: "..." }
POST /api/reviews/:id/request-changes?token=xxx Sets status='needs_changes', reviewed_by, reviewed_at. Body: required { note: "..." }
POST /api/reviews/:id/comments?token=xxx Add a comment. Body: { comment, section_ref?, original_text?, suggested_text? }

4b. Admin-Facing (JWT Auth — existing adminAuth middleware)

MethodEndpointWhat It Does
POST /api/admin/reviews Submit new content for review. Body: { title, content_type, content_body, context_brief, expert_score, brand_rules, live_url, submitted_by }
PUT /api/admin/reviews/:id Update content (creates new version if status was 'approved'). Body: same as POST.
GET /api/admin/reviews/dashboard Returns counts by status: { pending: N, approved: N, needs_changes: N, total: N }
POST /api/admin/reviews/:id/comments/:commentId/resolve Mark a comment as resolved (after Charlotte incorporates the feedback).
POST /api/admin/reviewers/rotate-token Regenerate a reviewer's token (security — invalidates old URL). Body: { email }

5. Frontend — Kristi's Review Interface

Route: /review (public route, token-authenticated via query param)

Design Priority

Mobile-first. Kristi reviews on her phone. Every element must be thumb-friendly: large tap targets, no hover states required, clear visual hierarchy. The desktop version is just a wider version of the mobile layout — not a separate design.

5a. Review List View

┌──────────────────────────────────┐ │ Stylify Content Review │ │ ─────────────────────────────── │ │ │ │ ┌─ 3 items need your review ──┐ │ │ │ │ │ │ │ ┌────────────────────────┐ │ │ │ │ │ ● Email 1: Welcome │ │ │ │ │ │ Email · Submitted 2d │ │ │ │ │ │ Panel: 8.2 │ │ │ │ │ └────────────────────────┘ │ │ │ │ │ │ │ │ ┌────────────────────────┐ │ │ │ │ │ ● Scorecard Quiz Copy │ │ │ │ │ │ Scorecard · Today │ │ │ │ │ │ Panel: 7.8 │ │ │ │ │ └────────────────────────┘ │ │ │ │ │ │ │ └──────────────────────────────┘ │ │ │ │ ┌─ Already reviewed (7) ──────┐ │ │ │ ✓ Lead Page Approved │ │ │ │ ✓ Email 3 Approved │ │ │ │ ⚑ Email 5 Changes │ │ │ └──────────────────────────────┘ │ └──────────────────────────────────┘

Visual design notes:

5b. Review Detail View

┌──────────────────────────────────┐ │ ← Back │ │ ─────────────────────────────── │ │ Scorecard Quiz Copy │ │ Scorecard · Panel: 7.8 · v1 │ │ │ │ ┌─ CONTEXT ─────────────────┐ │ │ │ This is the copy for the │ │ │ │ 12-question scorecard quiz │ │ │ │ at stylify-ai.com/scorecard│ │ │ │ [View Live ↗] │ │ │ └────────────────────────────┘ │ │ │ │ SECTION 1: INTRO SCREEN │ │ │ │ Headline: "How's Your Social │ │ Media Game?" │ │ Subtext: "Take the free..." │ │ │ │ ┌─ Tap to comment ──────────┐ │ │ │ 💬 Add a note here │ │ │ └────────────────────────────┘ │ │ │ │ SECTION 2: QUIZ QUESTIONS │ │ ... │ │ │ │ ═══════════════════════════════ │ │ │ │ ┌────────────┐ ┌──────────────┐ │ │ │ ✓ Approve │ │ ⚑ Needs │ │ │ │ │ │ Changes │ │ │ └────────────┘ └──────────────┘ │ │ │ │ ┌────────────────────────────┐ │ │ │ General notes (optional) │ │ │ │ │ │ │ └────────────────────────────┘ │ └──────────────────────────────────┘

Detail view features:

5c. Comment UI

┌──────────────────────────────────┐ │ 💬 Comment on: "CTA Headline" │ │ ─────────────────────────────── │ │ │ │ Selected text: │ │ "Ready to fix your social..." │ │ │ │ Your suggestion (optional): │ │ ┌────────────────────────────┐ │ │ │ Maybe "Ready to level up"? │ │ │ └────────────────────────────┘ │ │ │ │ Note: │ │ ┌────────────────────────────┐ │ │ │ This feels a little harsh │ │ │ │ — "fix" implies broken │ │ │ └────────────────────────────┘ │ │ │ │ ┌────────────┐ ┌──────────────┐ │ │ │ Cancel │ │ Add Comment │ │ │ └────────────┘ └──────────────┘ │ └──────────────────────────────────┘

Comment features:

6. Frontend — Jason's Admin View

Route: /admin/reviews (uses existing admin auth — adminAuth middleware)

This is a simple dashboard panel. Could be a new tab on the existing Six Engine Command Center, or a standalone page. Minimal build — just shows the status at a glance.

Dashboard Contents

Charlotte's Access

Charlotte (via browser automation) can use both the admin UI and the API endpoints. For submitting content, the admin UI provides a form. For reading Kristi's feedback programmatically, the API endpoints return structured JSON — far more reliable than trying to parse Google Docs suggestions.

7. Workflow: How It All Connects

7a. Submitting Content for Review

  1. Charlotte finishes content (email copy, scorecard text, landing page copy, etc.).
  2. Charlotte runs Expert Panel scoring → gets a score (e.g. 7.8/10).
  3. Charlotte submits to the review portal via:
    • Option A (API): POST /api/admin/reviews with all fields. Cleanest — Charlotte can do this from a Task subagent.
    • Option B (Browser): Charlotte navigates to /admin/reviews, fills the submission form, clicks Submit.
  4. The review appears in Kristi's queue immediately.

7b. Kristi Reviewing

  1. Kristi opens her bookmarked URL on her phone.
  2. She sees pending items at the top. Taps one.
  3. Reads the context brief, then the content.
  4. If something needs tweaking: she highlights text, types a suggestion or note.
  5. At the bottom: taps "Approve" or "Needs Changes" with an optional general note.
  6. Done. Back to the list. Next item.

7c. Charlotte Incorporating Feedback

  1. Charlotte reads all comments via GET /api/reviews/:id?token=admin (or API with admin JWT).
  2. For each comment with suggested_text: Charlotte makes the exact change, then marks the comment resolved.
  3. For general notes: Charlotte addresses them and marks resolved.
  4. Charlotte submits a new version (PUT /api/admin/reviews/:id → creates v2 with parent_id linking to v1).
  5. Kristi sees the new version in her queue for re-review.

7d. The Re-Approval Rule (Automated)

  1. Content is approved (v1, status='approved').
  2. Someone changes the content (Charlotte rewrites a line, Stitch changes a UI string, Pixel updates a caption).
  3. Charlotte creates a new review with parent_id = original's ID and version = 2.
  4. The original's status is automatically changed to 'revised' (indicating a newer version exists).
  5. Kristi sees the new version in her pending queue — it shows "v2 — changed by Charlotte" and includes a diff summary in the context brief.
  6. For typo fixes: the context brief can be as short as "Changed 'recieve' to 'receive' in paragraph 3."

8. Implementation Notes for Stitch

8a. File Structure

// New files
backend/src/migrations/022_content_review_portal.sql
backend/src/routes/reviewRoutes.js
backend/src/middleware/reviewerAuth.js

// Frontend (new components)
frontend/src/components/ReviewPortal.jsx      // Kristi's view
frontend/src/components/ReviewPortal.css
frontend/src/components/AdminReviews.jsx      // Jason's admin view (optional: add to CommandCenter)

8b. Reviewer Auth Middleware

// backend/src/middleware/reviewerAuth.js
const authenticateReviewer = async (req, res, next) => {
  const token = req.query.token;
  if (!token) return res.status(401).json({ error: 'Review token required' });

  const reviewer = await supabase
    .from('reviewers')
    .select('*')
    .eq('token', token)
    .eq('is_active', true)
    .single();

  if (!reviewer.data) return res.status(401).json({ error: 'Invalid review token' });

  req.reviewer = reviewer.data;
  next();
};

8c. Content Rendering

The content_body is stored as plain text with simple section markers. On the frontend, render with:

8d. Text Selection for Comments

On mobile, text selection is native (long-press). After selection, show a floating "Comment" button near the selection. Use the Selection API:

const handleTextSelection = () => {
  const selection = window.getSelection();
  if (selection.toString().trim()) {
    setSelectedText(selection.toString());
    // Find nearest section header for section_ref
    const range = selection.getRangeAt(0);
    const section = range.startContainer.parentElement
      .closest('[data-section]');
    setSectionRef(section?.dataset.section || 'General');
    setShowCommentBox(true);
  }
};

8e. Production Freeze Compatibility

Safe to Build Now

This is an internal tool that doesn't affect the customer-facing app or Meta's review process. The migration adds new tables (doesn't modify existing ones). The new routes are isolated. The frontend component loads on a new route. This can be built, tested, and deployed during the production freeze without any risk to Meta approval.

8f. What NOT to Build (Scope Control)

9. Migration from Google Docs

Once the review portal is live:

  1. Charlotte migrates existing pending items — re-submits each Google Doc review package as a content_review via the API. Includes any comments Kristi already left (manually transferred).
  2. Kristi gets her new bookmark URL — Jason sends it to her. One-time setup.
  3. Google Drive folder stays as archive — don't delete it. The Review Tracker spreadsheet can be retired since the portal has a built-in dashboard.
  4. CLAUDE.md updated — Charlotte's Content Approval System section points to the portal instead of Google Docs.

10. Future Enhancements (v2+)

Not for initial build. Documenting so we don't forget:

11. Open Decisions for Jason

Decisions Needed
  1. Kristi notification: Should we text Kristi when something new needs review, or is checking the bookmark enough? (Recommend: start with just the bookmark, add notifications in v2 if needed.)
  2. Admin review panel location: New standalone page at /admin/reviews, or a new tab on the existing Six Engine Command Center? (Recommend: standalone page for now — simpler build.)
  3. Priority relative to other Stitch work: This doesn't block launch, but it permanently fixes the review workflow. Recommend building after the current grade-improvement queue but before launch push. Slot: ~March 1–3 if Meta approval hasn't landed yet.

12. Artifact Self-Check

Assumption: Kristi's phone browser supports the Selection API for text highlighting. All modern mobile browsers (Safari iOS, Chrome Android) support this, but the floating "Comment" button behavior after selection may need testing on her specific device. Fallback: section-level comments (tap between sections) work on any device.

Uncertainty: Whether Charlotte should submit content via the API directly (requires making HTTP requests from Claude CoWork, which isn't currently straightforward) or via the browser UI (which we know works but is slower). The spec supports both paths — Stitch should build both the API and the form UI so we can use whichever works better in practice.