NowPage Access Control PRD


Date: 2026-03-10

Owner: Jason

Status: Ready to Build

Parent: FORGE-PRD-v2.md (format reference)


1. What Is This?


Per-user domain gating for the NowPage MCP server. Right now, ALLOWED_DOMAINS is a global env var — every MCP user can publish to every allowed domain. This adds a user-level mapping so each person can only publish to their assigned domain(s). Also adds usage logging, the ability to disconnect users, and a dashboard view of who's connected.


Core principle: Jason controls who publishes where. Users get a domain, they publish to that domain, they can't touch anyone else's.


2. Architecture Overview



┌─────────────────────────────────────────────────────┐
│                  NowPage MCP Server                  │
│              (nowpage-mcp.vercel.app)                │
│                                                      │
│  ┌─────────┐    ┌──────────┐    ┌────────────────┐  │
│  │  Auth    │───▶│  Access  │───▶│  Publish Tool  │  │
│  │ (BYOK)  │    │  Guard   │    │  (existing)    │  │
│  └─────────┘    └──────────┘    └────────────────┘  │
│       │              │                               │
│       │         ┌────▼─────┐                         │
│       │         │ Supabase │                         │
│       │         │ Tables:  │                         │
│       │         │ • users  │                         │
│       │         │ • logs   │                         │
│       │         └──────────┘                         │
│       │                                              │
│  Bearer Token = npKey:resendKey                      │
│  npKey used to look up user → allowed_domains        │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│              Forge Dashboard (:3002)                 │
│                                                      │
│  GET /api/mcp-users    → list users + domains        │
│  GET /api/mcp-logs     → usage log (who published)   │
│  POST /api/mcp-users   → add/edit/disable user       │
└─────────────────────────────────────────────────────┘

3. Specification


3.1 Supabase Tables



-- User-to-domain mapping
CREATE TABLE mcp_users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,              -- "Joe", "Lee", "Sumit"
  np_key TEXT UNIQUE NOT NULL,     -- their NowPage API key (Bearer token first half)
  allowed_domains TEXT[] NOT NULL, -- {'joe.asapai.net'}
  enabled BOOLEAN DEFAULT true,   -- false = disconnected
  created_at TIMESTAMPTZ DEFAULT now(),
  disabled_at TIMESTAMPTZ,        -- when they were disconnected
  notes TEXT                       -- "BitDevs March 10" etc
);

-- Usage log
CREATE TABLE mcp_publish_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES mcp_users(id),
  user_name TEXT,                  -- denormalized for quick reads
  domain TEXT NOT NULL,
  slug TEXT NOT NULL,
  action TEXT,                     -- 'published', 'blocked', 'error'
  blocked_reason TEXT,             -- 'domain_not_allowed', 'user_disabled'
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Index for dashboard queries
CREATE INDEX idx_mcp_log_created ON mcp_publish_log(created_at DESC);
CREATE INDEX idx_mcp_log_user ON mcp_publish_log(user_id);

3.2 Access Guard (MCP Server Change)


File: apps/nowpage-mcp/lib/access-guard.ts


Purpose: Look up bearer token → user → check domain is allowed + user is enabled.


Behavior:


Interface:


interface AccessResult {
  allowed: boolean;
  userId?: string;
  userName?: string;
  userDomains?: string[];
  error?: string;
}

async function checkAccess(
  npKey: string | undefined,
  requestedDomain: string
): Promise<AccessResult>

3.3 MCP Route Changes


File: apps/nowpage-mcp/app/api/mcp/route.ts


Changes:


3.4 Dashboard API Endpoints


File: commander/observe/dashboard.ts (existing Forge dashboard)


Endpoints:


EndpointMethodDescription
/api/mcp-usersGETList all MCP users with status, domains, last publish
/api/mcp-usersPOSTAdd or update a user (name, np_key, allowed_domains, enabled)
/api/mcp-users/:id/disablePOSTDisconnect a user (set enabled=false)
/api/mcp-users/:id/enablePOSTRe-enable a user
/api/mcp-logsGETUsage log with filters (?user=&domain=&limit=)

3.5 Wizard Integration


File: apps/nowpage-mcp/app/api/demo-wizard/route.ts


Change: After creating a demo for someone, optionally auto-create an mcp_users row with their demo domain. This way when Jason creates "Bob's demo" from the wizard, Bob is pre-registered for demo.asapai.net and ready to connect the MCP.


New optional field in wizard: createMcpUser: boolean (default false). If true, generates a random np_key, creates the user row, and includes the MCP setup URL with their key in the setup page.


4. Data Model


See Section 3.1 for SQL. Two tables: mcp_users (user-domain mapping) and mcp_publish_log (audit trail).


Key relationships:


5. Execution Phases


Phase 1: Database + Access Guard (~2 Ralph-hours)


> Core gating logic. After this, per-user domain restrictions work.


  1. Create Supabase migration with mcp_users and mcp_publish_log tables
  2. Files: SQL migration
  3. Acceptance: Tables exist, can insert/query

    1. Seed initial users: Joe (joe.asapai.net), Lee (lee.asapai.net), Sumit (sumit.asapai.net)
    2. Files: SQL seed script
    3. Acceptance: SELECT * FROM mcp_users returns 3 rows with correct domains

      1. Build apps/nowpage-mcp/lib/access-guard.ts
      2. Files: lib/access-guard.ts, add @supabase/supabase-js to package.json
      3. Acceptance: checkAccess("joes-key", "joe.asapai.net") returns allowed=true, checkAccess("joes-key", "lee.asapai.net") returns allowed=false

        1. Wire access guard into MCP publish handler
        2. Files: app/api/mcp/route.ts
        3. Acceptance: Publishing to wrong domain returns error with user's allowed domain listed

          1. Add publish logging to mcp_publish_log
          2. Files: lib/access-guard.ts
          3. Acceptance: Every publish attempt (success or blocked) creates a log row

          4. HUMAN REQUIRED: Set SUPABASE_URL and SUPABASE_SERVICE_KEY env vars on Vercel for the NowPage MCP project (if not already set).


            Done when: Joe can publish to joe.asapai.net but gets blocked on lee.asapai.net. All attempts logged.


            Phase 2: Dashboard + Management (~1 Ralph-hour)


            > Visibility and control. After this, Jason can manage users from the dashboard.


            1. Add /api/mcp-users GET endpoint to dashboard
            2. Files: commander/observe/dashboard.ts
            3. Acceptance: curl localhost:3002/api/mcp-users returns JSON array of users

              1. Add /api/mcp-users POST endpoint (add/edit user)
              2. Files: commander/observe/dashboard.ts
              3. Acceptance: POST creates new user, PUT updates existing

                1. Add /api/mcp-users/:id/disable and /enable endpoints
                2. Files: commander/observe/dashboard.ts
                3. Acceptance: Disabling user → their next MCP publish attempt is rejected

                  1. Add /api/mcp-logs GET endpoint
                  2. Files: commander/observe/dashboard.ts
                  3. Acceptance: Returns recent publish attempts with user name, domain, action, timestamp

                  4. Done when: Jason can list users, disable/enable them, and see publish logs from the dashboard.


                    Phase 3: Wizard Auto-Registration (~30 min)


                    > When Jason creates a demo, optionally pre-register the person as an MCP user.


                    1. Add createMcpUser option to wizard endpoint
                    2. Files: app/api/demo-wizard/route.ts
                    3. Acceptance: Creating a demo with createMcpUser:true creates an mcp_users row

                      1. Return generated np_key in wizard response
                      2. Files: app/api/demo-wizard/route.ts
                      3. Acceptance: Response includes mcpKey field when user created

                        1. Update setup page to include personalized MCP URL with key
                        2. Files: app/api/demo-wizard/route.ts (generateSetupPage)
                        3. Acceptance: Setup page shows user's personal connection URL (still gated/blurred until payment)

                        4. Done when: Wizard can auto-register MCP users, setup page shows their personal key.


                          6. Cost Model


                          ComponentProviderMonthly Cost
                          Supabase tables (2 tables, low volume)Supabase$0 (free tier)
                          Access guard queries (~100/day)Supabase$0 (free tier)
                          Total$0/mo

                          7. Security Considerations


                          • np_key is the auth token. Treat it like a password. Don't expose it in logs or error messages (only show first 8 chars in dashboard).
                          • Supabase service key must be server-side only (env var, never client-side).
                          • RLS not needed — these tables are only accessed server-side via service key.
                          • Rate limiting — not in v1. If needed later, add per-user publish rate limits in access guard.
                          • Key rotation — not in v1. If a key is compromised, disable the user and create a new one.

                          8. Definition of Done


                          • Joe can publish to joe.asapai.net via MCP
                          • Joe CANNOT publish to lee.asapai.net via MCP (gets clear error with his allowed domain)
                          • Disabled user gets "Access revoked" error on publish attempt
                          • Every publish attempt (success or blocked) logged in mcp_publish_log
                          • GET /api/mcp-users returns user list with domains and status
                          • POST /api/mcp-users/:id/disable prevents user from publishing
                          • Wizard works unchanged (uses server NP_API_KEY, bypasses access guard)
                          • Dashboard shows recent publish log with who, where, when

                          Ralph Invocation


                          
                          # Phase 1
                          ralph.sh nowpage-access-control "Phase 1, Task 1: Create Supabase migration for mcp_users and mcp_publish_log tables"
                          ralph.sh nowpage-access-control "Phase 1, Task 2: Seed initial users Joe/Lee/Sumit with their domains"
                          ralph.sh nowpage-access-control "Phase 1, Task 3: Build access-guard.ts with checkAccess function"
                          ralph.sh nowpage-access-control "Phase 1, Task 4: Wire access guard into MCP publish handler"
                          ralph.sh nowpage-access-control "Phase 1, Task 5: Add publish logging"
                          
                          # Phase 2
                          ralph.sh nowpage-access-control "Phase 2, Task 1: Dashboard GET /api/mcp-users endpoint"
                          ralph.sh nowpage-access-control "Phase 2, Task 2: Dashboard POST /api/mcp-users endpoint"
                          ralph.sh nowpage-access-control "Phase 2, Task 3: Dashboard disable/enable endpoints"
                          ralph.sh nowpage-access-control "Phase 2, Task 4: Dashboard GET /api/mcp-logs endpoint"
                          
                          # Phase 3
                          ralph.sh nowpage-access-control "Phase 3, Task 1: Wizard createMcpUser option"
                          ralph.sh nowpage-access-control "Phase 3, Task 2: Return np_key in wizard response"
                          ralph.sh nowpage-access-control "Phase 3, Task 3: Personalized setup page with user key"
                          

                          NowPage Access Control PRD — March 10, 2026

                          Powered by NowPage