AI Prompt: Forma3D.Connect — Phase: RBAC + Tenant-Ready (Single Tenant)¶
Purpose: Instruct an AI (Claude Opus 4.5) to implement Role-Based Access Control (RBAC) and tenant-ready data isolation for Forma3D.Connect
Estimated Effort: 18–30 hours (implementation + tests)
Output: Multi-user authentication + RBAC enforced across API and dashboard, and tenant-aware persistence using one default tenant (no customer multi-tenant UX yet)
Status: 🚧 TODO
🎯 Mission¶
Implement RBAC and tenant-ready multi-tenancy in Forma3D.Connect.
Critical constraints:
- Do NOT integrate Keycloak or OpenID Connect yet.
- Implement in-app authentication (local users) and in-app RBAC (roles/permissions).
- Implement tenant-aware schema and repository enforcement, but run with exactly one default tenant for now.
- Preserve the existing operational model: webhooks and machine integrations should remain functional.
Deliverables:
- Multiple user accounts can sign in to the dashboard.
- Each user has one or more roles and effective permissions.
- API endpoints enforce permissions server-side (no UI-only security).
- Database writes/reads are tenant-scoped through repositories.
- System runs single-tenant by default (one tenant seeded/ensured).
📌 Context (Current State)¶
- The dashboard currently uses a single shared API key login concept.
- The backend is NestJS + Prisma + PostgreSQL with repository pattern expectations.
- Webhooks exist for Shopify and SimplyPrint (and other integrations).
Goal of this phase: introduce RBAC and tenant boundaries without prematurely adding external identity providers.
🛠️ Tech Stack Reference¶
- Backend: NestJS (TypeScript), Prisma, PostgreSQL
- Frontend: React 19 + React Router + TanStack Query
- Monorepo: Nx + pnpm
- Testing: Jest (API), Vitest (web), Playwright acceptance tests
🏗️ Architecture Requirements¶
1) Layering Rules (must respect)¶
- Controllers are HTTP-only
- Services hold business logic
- Repositories hold Prisma access
- No Prisma calls outside repositories
- DTOs required for all IO
1b) Observability + Auditing (must implement)¶
- All security-relevant actions must be audited (persisted) with actor identity + tenant context.
- Identity context must be attached to Sentry (frontend + backend) where possible.
- Never log sensitive secrets (passwords, tokens, API keys). Redact where needed.
2) RBAC model (explicit, non-magical)¶
- Permissions are string constants (stable identifiers), e.g.
orders.read,orders.write. - Roles are named bundles of permissions, e.g.
admin,operator,viewer. - Users can have multiple roles (effective permissions = union).
- Authorization uses typed errors and no silent failures.
3) Tenant model (tenant-ready, single tenant now)¶
- Add a
Tenanttable. - Add
tenantIdforeign keys to all tenant-owned data. - Enforce tenant scoping in repositories and service entrypoints.
- System behavior defaults to a single seeded tenant (e.g.
default).
📁 Files to Create/Modify (high-level)¶
Backend (NestJS)¶
apps/api/src/
├── auth/
│ ├── dto/
│ │ ├── login.dto.ts
│ │ ├── session.dto.ts
│ │ └── user.dto.ts
│ ├── guards/
│ │ ├── session.guard.ts
│ │ └── permissions.guard.ts
│ ├── decorators/
│ │ ├── current-user.decorator.ts
│ │ └── require-permissions.decorator.ts
│ ├── services/
│ │ ├── auth.service.ts
│ │ ├── password-hasher.service.ts
│ │ └── permission.service.ts
│ ├── auth.controller.ts
│ ├── auth.module.ts
│ └── permissions.ts
│
├── audit/
│ ├── dto/
│ │ └── audit-event.dto.ts
│ ├── audit.repository.ts
│ ├── audit.service.ts
│ └── audit.module.ts
│
├── tenancy/
│ ├── tenant-context.ts
│ ├── tenant-context.service.ts
│ ├── tenant.resolver.ts
│ └── tenancy.module.ts
│
├── users/
│ ├── dto/
│ │ ├── create-user.dto.ts
│ │ ├── update-user-roles.dto.ts
│ │ └── user-list.dto.ts
│ ├── users.controller.ts
│ ├── users.service.ts
│ ├── users.repository.ts
│ └── users.module.ts
│
└── (existing modules updated to enforce permissions + tenant scoping)
Frontend (React dashboard)¶
apps/web/src/
├── auth/
│ ├── api/
│ │ └── auth.api.ts
│ ├── hooks/
│ │ ├── use-auth.ts
│ │ └── use-permissions.ts
│ ├── auth-context.tsx
│ └── types.ts
│
├── pages/
│ ├── login/ (update to support user login)
│ └── admin/
│ └── users/ (minimal user management UI)
│
└── (routes updated to gate admin pages by permission)
Database (Prisma)¶
prisma/schema.prisma # UPDATE: add Tenant, User, Role, Permission, join tables, tenantId FKs
prisma/seed.ts # UPDATE: ensure default tenant + initial admin user/role
prisma/migrations/ # NEW migration(s)
🔐 Authentication Approach (No Keycloak Yet)¶
Implement local user authentication with secure password hashing and server-side sessions.
Requirements¶
- Use httpOnly cookie sessions (recommended) OR short-lived JWT + refresh token (cookie). Prefer a minimal, secure approach.
- Passwords must be hashed with argon2 (preferred) or bcrypt with strong settings.
- Include secure cookie settings:
httpOnly: truesecure: truein productionsameSite: 'lax'(or 'strict' if feasible)- Provide endpoints:
POST /api/v1/auth/loginPOST /api/v1/auth/logoutGET /api/v1/auth/me
Backward compatibility (important)¶
- Keep the current API key mechanism available as a legacy operator bootstrap for now, but:
- Gate it behind an env flag (e.g.
AUTH_ALLOW_LEGACY_API_KEY=true). - Treat legacy API key sessions as a specific principal with a very explicit role (e.g.
legacy-admin) and log usage.
🧾 Auditing Requirements (Security + Compliance)¶
What to audit (minimum set)¶
Audit events must be persisted (database) for:
- Authentication:
- login success
- login failure (without storing passwords)
- logout
- Authorization:
- permission denied (include required permissions and route)
- User & Role administration:
- user created / deactivated
- password reset / password changed
- roles assigned/removed
- role created/updated
- Tenant context changes (future-proof):
- tenant created (even if only default tenant exists now)
Audit event fields (required)¶
Every audit record must include:
tenantIdactorUserId(nullable for unauthenticated events like failed login)actorEmail(nullable)actorRoles(array or computed snapshot)action(string identifier, e.g.auth.login.success)targetType+targetId(nullable, for actions on specific entities)successbooleanipAddressanduserAgentrequestId/ correlation id (if available)createdAtmetadata(JSON) for structured details (never secrets)
Implementation notes¶
- Prefer integrating with the existing logging/event infrastructure if present, but do not conflate “business event logs” with “security audit logs” unless the model cleanly supports the required fields above.
- Audit logging must be best-effort: a failure to write an audit record must not crash webhook processing, but it must be reported to Sentry (with identity context if available).
✅ RBAC Requirements¶
Permissions (explicit list)¶
Create apps/api/src/auth/permissions.ts with constants such as:
- Orders:
orders.readorders.write- Print Jobs:
printJobs.readprintJobs.write- Product Mappings:
mappings.readmappings.write- Shipments:
shipments.readshipments.write- Logs/Observability:
logs.readobservability.read- Admin:
users.readusers.writeroles.readroles.write
Roles (initial defaults)¶
Seed at least:
admin: all permissionsoperator: day-to-day operations (read/write orders, print jobs, shipments, mappings; read logs)viewer: read-only access (read orders/print jobs/shipments/mappings/logs)
Enforcement (server-side)¶
- Implement
@RequirePermissions(...)decorator and aPermissionsGuard. - Apply it to all sensitive controllers:
- Orders, Print Jobs, Shipments, Product Mappings, Event Logs, Settings/Admin.
- The guard must compute effective permissions from the authenticated user’s roles.
🧩 Multi-Tenancy (Tenant-Ready, Single Tenant Now)¶
Tenant resolution rules (for now)¶
- For authenticated users: tenant is
user.tenantId. - For legacy API key principal: tenant is the single default tenant.
- For webhooks (Shopify/SimplyPrint): assign events to the single default tenant for now, but implement the code so it can later map:
- Shopify:
x-shopify-shop-domain -> tenantId - SimplyPrint: per-tenant token or per-tenant webhook path (future)
Repository enforcement (non-negotiable)¶
- Every repository method that reads/writes tenant-owned entities must require a
tenantIdinput (or aTenantContextobject). - Do not allow “global” reads by default.
- Add appropriate compound indexes/unique constraints using tenant scoping, e.g.:
(tenantId, shopifyOrderId)unique(tenantId, webhookId)for idempotency where relevant
🧭 Identity Context Propagation (Web → API) + Sentry Enrichment¶
Goal¶
Ensure the same identity context established in the UI (after login) flows into the API handling and is applied consistently to:
- backend logs and Sentry events
- frontend Sentry events
- audit events
Backend requirements (Sentry scope)¶
On each authenticated request, enrich Sentry scope with:
user.id,user.email- tags:
tenantId,userId - context:
roles(and optionallypermissionsif reasonably small) - request context:
requestId, route/controller/action if available
Also:
- clear user context for unauthenticated requests
- ensure permission-denied exceptions include required permission(s) in Sentry breadcrumbs/tags (but not secrets)
Frontend requirements (Sentry scope)¶
When the user is authenticated in the dashboard:
- call Sentry
setUser({ id, email }) - set tags/contexts:
tenantId,roles
When the user logs out:
- clear Sentry user/context (
setUser(null)), clear related tags/contexts
“Identity context coming from the UI should also flow into the API”¶
Implement this in a way that remains compatible with secure session cookies:
- If using cookie sessions, the API must derive identity from the server-side session, but still propagate:
- a stable
requestId/ correlation id - (optional) a lightweight header like
x-client-request-idfor debugging (must be non-secret) - Ensure the API uses the authenticated principal as the source of truth for identity (never trust client-provided userId/roles/tenantId).
🧪 Testing Requirements¶
Backend unit tests (Jest)¶
Add/extend tests for:
- Auth login/logout/me
- Password hashing verification (no plaintext comparisons)
- Permissions guard:
- allows when permission present
- denies when missing
- denies when unauthenticated
- Tenant context resolution:
- user tenant
- legacy API key tenant
- webhook default tenant
- Repository scoping:
- queries include
tenantIdfilter -
uniqueness constraints are tenant-scoped
-
Auditing:
- audit records are created for login success/failure, logout
- audit record is created for permission denied
-
audit records include tenantId + actor fields correctly
-
Sentry identity context:
- Sentry scope contains user + tenant tags on authenticated requests (mock Sentry client)
- Sentry scope is cleared/empty on unauthenticated requests
Frontend tests (Vitest)¶
- Auth context:
- login success stores user + permissions
- logout clears session state
- Route gating:
-
admin users page requires
users.read(orusers.writefor edit actions) -
Sentry identity context:
- login sets Sentry user + tenant tags
- logout clears Sentry user/context
Acceptance tests (Playwright + Gherkin)¶
Add scenarios:
- Admin can log in and access Users page
- Viewer cannot access Users page (gets denied/redirected)
- Operator cannot perform admin-only actions
- Unauthorized actions are denied and audited (validate via API or admin audit log UI if added)
✅ Validation Checklist¶
-
pnpm nx build apisucceeds -
pnpm nx build websucceeds -
pnpm lintpasses - No TypeScript errors
- Prisma migration runs cleanly
- Default tenant is present in DB
- At least one admin user exists (seeded)
- API enforces permissions (cannot bypass via direct HTTP calls)
- Webhooks still work (processed under default tenant)
🎬 Execution Order¶
- Add Prisma models:
Tenant,User,Role,Permission(+ join tables) and tenant foreign keys - Create migration + update seed:
- ensure default tenant
- seed
admin/operator/viewerroles and permissions - seed initial admin user with secure password
- Implement backend auth:
- session/cookie auth
/auth/login,/auth/logout,/auth/me- Implement backend RBAC:
- permission constants
- permissions computation
@RequirePermissions+ guard- apply to controllers
- Implement tenancy enforcement:
- tenant context
- repository scoping rules
- update existing repositories/services accordingly
- Update web app auth:
- login with user credentials
- auth context + protected routes
- permission-aware navigation/UI
- Add minimal admin UI for user management (create user, assign roles)
- Add tests (unit + acceptance) and ensure they pass
📊 Expected Output¶
Verification Commands¶
# Database
pnpm prisma migrate dev
pnpm prisma db seed
# Build
pnpm nx build api
pnpm nx build web
# Lint + tests
pnpm lint
pnpm test
pnpm nx test acceptance-tests
Success Criteria¶
- You can create at least 3 users (admin/operator/viewer) and verify:
- Admin can manage users and roles.
- Operator can perform operational tasks but cannot manage users.
- Viewer can only read.
- All tenant-owned data is automatically scoped to the (single) default tenant.
- The codebase is tenant-ready for future multi-tenant enablement (mapping integrations to tenants later).
📝 Documentation Updates¶
Update docs to reflect:
- How to create users / rotate the legacy API key (if still enabled)
- How RBAC permissions and roles are defined
- How tenant scoping works (today: default tenant; future: multiple)
- How webhook tenant mapping will evolve (Shopify shop domain mapping; SimplyPrint token/path strategy)
🚫 Constraints and Rules¶
MUST DO¶
- Use strict typing; do not introduce
any,unknown,ts-ignore, or eslint disables - Enforce RBAC server-side with guards
- Enforce tenant scoping in repositories
- Keep webhooks functional (default tenant for now)
- Provide a safe bootstrap path (seeded admin user)
MUST NOT¶
- Do not add Keycloak/OIDC now
- Do not store secrets in client code
- Do not rely on UI-only gating for security
- Do not introduce “magic” implicit tenant selection; tenant context must be explicit and testable
END OF PROMPT
This prompt implements RBAC and tenant-ready persistence (single default tenant today) for Forma3D.Connect. The AI should implement the full end-to-end feature set (API + web + database + tests) with minimal disruption to existing webhook/integration flows, and without introducing external identity providers yet.