Date: 2026-03-10
Owner: Jason
Status: Ready to Build
Parent: FORGE-PRD-v2.md (format reference)
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.
┌─────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────┘
-- 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);
File: apps/nowpage-mcp/lib/access-guard.ts
Purpose: Look up bearer token → user → check domain is allowed + user is enabled.
Behavior:
NP_API_KEY env → owner mode, no restrictions (wizard, Forge services)np_key in mcp_users tableenabled = false → reject with "Access revoked"allowed_domains → reject with "Domain not allowed. Your domain: X"mcp_publish_logInterface:
interface AccessResult {
allowed: boolean;
userId?: string;
userName?: string;
userDomains?: string[];
error?: string;
}
async function checkAccess(
npKey: string | undefined,
requestedDomain: string
): Promise<AccessResult>
File: apps/nowpage-mcp/app/api/mcp/route.ts
Changes:
checkAccess from access-guardpublish_to_nowpage tool handler, after domain resolution, call checkAccess(npKey, domain)send_email and register_to_command_center unchanged (no domain gating needed)File: commander/observe/dashboard.ts (existing Forge dashboard)
Endpoints:
| Endpoint | Method | Description |
|---|---|---|
| /api/mcp-users | GET | List all MCP users with status, domains, last publish |
| /api/mcp-users | POST | Add or update a user (name, np_key, allowed_domains, enabled) |
| /api/mcp-users/:id/disable | POST | Disconnect a user (set enabled=false) |
| /api/mcp-users/:id/enable | POST | Re-enable a user |
| /api/mcp-logs | GET | Usage log with filters (?user=&domain=&limit=) |
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.
See Section 3.1 for SQL. Two tables: mcp_users (user-domain mapping) and mcp_publish_log (audit trail).
Key relationships:
mcp_users.np_key → matches first half of Bearer token in MCP requestsmcp_publish_log.user_id → FK to mcp_users for joinsmcp_users.allowed_domains → Postgres TEXT array, checked with @> operator> Core gating logic. After this, per-user domain restrictions work.
mcp_users and mcp_publish_log tablesSELECT * FROM mcp_users returns 3 rows with correct domainsapps/nowpage-mcp/lib/access-guard.tslib/access-guard.ts, add @supabase/supabase-js to package.jsoncheckAccess("joes-key", "joe.asapai.net") returns allowed=true, checkAccess("joes-key", "lee.asapai.net") returns allowed=falseapp/api/mcp/route.tsmcp_publish_loglib/access-guard.tsHUMAN 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.
> Visibility and control. After this, Jason can manage users from the dashboard.
/api/mcp-users GET endpoint to dashboardcommander/observe/dashboard.tscurl localhost:3002/api/mcp-users returns JSON array of users/api/mcp-users POST endpoint (add/edit user)commander/observe/dashboard.ts/api/mcp-users/:id/disable and /enable endpointscommander/observe/dashboard.ts/api/mcp-logs GET endpointcommander/observe/dashboard.tsDone when: Jason can list users, disable/enable them, and see publish logs from the dashboard.
> When Jason creates a demo, optionally pre-register the person as an MCP user.
createMcpUser option to wizard endpointapp/api/demo-wizard/route.tscreateMcpUser:true creates an mcp_users rowapp/api/demo-wizard/route.tsmcpKey field when user createdapp/api/demo-wizard/route.ts (generateSetupPage)Done when: Wizard can auto-register MCP users, setup page shows their personal key.
| Component | Provider | Monthly Cost |
|---|---|---|
| Supabase tables (2 tables, low volume) | Supabase | $0 (free tier) |
| Access guard queries (~100/day) | Supabase | $0 (free tier) |
| Total | $0/mo |
GET /api/mcp-users returns user list with domains and statusPOST /api/mcp-users/:id/disable prevents user from publishing
# 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