apps/workspace is the internal-only Next.js app deployed at workspace.matters.ai. It
hosts every admin module, content studio, and operational tool the team uses.
The workspace lives entirely under the (workspace) route group at
apps/workspace/app/(workspace)/**. The route group is unwrapped at URL level, so a file at
apps/workspace/app/(workspace)/webinars/page.tsx serves workspace.matters.ai/webinars.
| Module | Path | Owner permission |
|---|---|---|
| Dashboard | / | Any authenticated allowlisted user |
| Analytics | /analytics | org:analytics:view |
| Audience | /audience | org:leads:view |
| Careers | /careers | org:hrm:jobs_view |
| Content Studio | /content | org:content:view |
| Datasheets | /datasheets | org:content:view |
| Family | /family | org:sys:manage |
| Glossary | /glossary | org:glossary:view |
| Industries | /industries | org:content:view |
| Key Features | /key-features | org:content:view |
| Legal | /legal | org:legal:audit_view |
| Platform | /platform | org:content:view |
| Profile | /profile | Self-only |
| Sales | /sales | org:analytics:view |
| Settings | /settings | org:sys:manage |
| System | /system | org:sys:manage |
| System Health | /observability | org:observability:view |
| Use Cases | /use-cases | org:content:view |
| Videos | /videos | org:content:view |
| Webinars | /webinars | org:webinars:view |
| Whitepapers | /whitepapers | org:content:view |
A live, code-derived permission matrix lives at Workspace → RBAC.
Edge middleware (apps/workspace/proxy.ts):
clerkMiddleware() wraps every request.ALLOWED_HOSTS
(workspace.matters.ai, plus localhost/127.0.0.1/::1) get a bare 404 before any
auth runs./auth/*, /api/auth/2fa/*, /api/webhooks/clerk, /api/health) pass
through. /api/auth/2fa/* is public because it runs during the login step, before the
session is fully trusted; its handlers self-enforce Clerk auth().@matters/auth's ALLOWED_EMAIL_DOMAINS. Failures → 401/403 (API) or
/auth/login redirect (page).Layout defense-in-depth (apps/workspace/app/(workspace)/layout.tsx):
currentUser().getCurrentUserRoles() so the shell can hide unauthorized
modules from the sidebar./auth/sign-out?error=domain_restricted if a session somehow slipped past
the middleware.Per-route RBAC: every API handler that reads or mutates data calls checkRole(...) or
hasPermission(roles, ...) from lib/auth/rbac.ts / lib/auth/rbac-shared.ts. There are
13 roles and 55 permissions. See RBAC for the full matrix.
The workspace previously lived at matters.ai/workspace. Splitting it onto its own subdomain
delivers four wins:
workspace.matters.ai is internal" is a one-sentence
security boundary that ops, legal, and on-call all understand.pnpm --filter @matters/workspace dev # serves on :4001
Requires the same .env.local at repo root that the public site uses. The workspace app reads
all CLERK_*, MONGODB_*, WORDPRESS_*, MUX_*, SALESFORCE_*, etc. via the shared
@matters/lib/config/env Zod schema.
Clerk's session cookie needs
Domain=.matters.aiset in the Clerk dashboard for production SSO to work acrossmatters.ai↔workspace.matters.ai. Locally that's irrelevant; both apps sharelocalhost.