Spaces:
Running
MediaRouter Project Backend Foundation P1
Architecture
Project is the authoritative, durable workspace-owned container for future creative work. P1 introduces only the parent domain. It does not attach assets, generation jobs, templates, publishing records, timelines, analytics, or automations.
The implementation follows the existing backend boundaries:
FastAPI router
-> API-key authentication and method-specific scope policy
-> ProjectService (validation, lifecycle, audit coordination)
-> ProjectRepository (tenant-scoped SQLAlchemy access)
-> SecurityDatabase.tenant_session (PostgreSQL RLS context)
-> projects
No client request controls id, workspace_id, created_by, status on creation, or any
timestamp. workspace_id and user_id come only from the authenticated API-key principal and
its active workspace membership.
Database
Migration: app/projects/migrations/0001_projects_foundation.sql
The migration is additive and must run after the authoritative tenancy migration. MediaRouter's
existing tenancy, user, and asset primary keys are text columns containing UUID values. Projects
use that same representation so their foreign keys remain type-compatible; IDs are generated by
gen_random_uuid()::text in PostgreSQL and uuid4() in local SQLAlchemy-managed SQLite.
projects contains:
| Column | Contract |
|---|---|
id |
Server-generated UUID value and primary key |
workspace_id |
Required FK to workspaces, immutable |
created_by |
Required FK to users, immutable |
name |
Normalized non-empty text, maximum 200 characters |
description |
Nullable text, maximum 4,000 characters |
status |
active or archived only |
thumbnail_asset_id |
Nullable FK to authoritative media_assets |
metadata |
JSON object, limited to 16 KiB by the service |
| timestamps | Server-controlled creation, update, and archive timestamps |
Indexes cover workspace lookup, workspace/status, workspace/update ordering, and creator. There is no name uniqueness constraint: duplicate project names are valid. No child-domain foreign keys or cascading project deletion is introduced.
The migration also adds generic audit_events storage behind the existing AuditService. This
extends the shared audit boundary instead of introducing a project-only audit subsystem.
RLS and tenant isolation
projects and audit_events both enable and force PostgreSQL row-level security. Project policies
are command-specific:
- SELECT requires
app.workspace_id,app.user_id, and an active membership. - INSERT additionally requires
created_by = app.user_id. - UPDATE requires the same active workspace membership; a trigger makes ownership fields immutable.
- DELETE can only target the active RLS workspace. The public API never issues a hard delete.
Every repository query also includes an explicit workspace_id predicate, so local SQLite tests
and privileged backend roles retain the same application-level isolation. Missing and foreign IDs
both return PROJECT_NOT_FOUND, preventing IDOR probes from distinguishing another workspace's
records.
The project ownership trigger rejects a thumbnail whose canonical asset belongs to another workspace, including writes made by a privileged service role.
Permissions and API keys
The shared scope registry and middleware policy define:
projects:read— list and retrieveprojects:create— createprojects:update— patchprojects:delete— archive through DELETE
Developer and operator roles receive all four. Viewer receives read only. Administrators continue
to resolve through the existing admin scope behavior.
Frontend Architecture v2 already uses projects:write for aggregate capability rendering. The
scope remains a compatibility aggregate only: it expands to create/update/delete, while every HTTP
method is enforced against its specific scope. An API key may be granted only projects:create
without gaining update or delete access.
REST API
All endpoints are authenticated, rate-limited, request-audited, and represented in OpenAPI.
GET /v1/projects
Query parameters:
status:active(default) orarchivedsearch: case-insensitive name/description search over the loaded workspacelimit: 1–100, default 50cursor: opaque continuation token
Results are ordered by (updated_at DESC, id DESC). The response contains items, limit, and
next_cursor. The cursor encodes the last ordering pair but is opaque to callers; malformed
cursors return PROJECT_INVALID_CURSOR.
POST /v1/projects
Accepts only name, description, thumbnail_asset_id, and metadata. Names have internal
whitespace collapsed and are never truncated. Unknown/system fields are rejected.
GET /v1/projects/{project_id}
Returns the safe project API schema. workspace_id is included because the authenticated caller
already receives its authoritative workspace in /v1/auth/context; it is never accepted as input.
PATCH /v1/projects/{project_id}
Accepts only name, description, thumbnail_asset_id, metadata, and status. Setting status
to archived records archived_at. Archived projects are immutable in P1 and return
PROJECT_ALREADY_ARCHIVED on subsequent mutations. Restore is not implemented.
DELETE /v1/projects/{project_id}
Returns 204 and archives the project. It never hard-deletes or cascades. A repeated delete returns
PROJECT_ALREADY_ARCHIVED.
Thumbnail handling
P1 does not invent a new generic asset collection. A thumbnail is supported only when the ID
resolves to the existing authoritative media_assets table and belongs to the authenticated
workspace. The API stores the stable asset ID, not a filesystem path or arbitrary URL. Invalid,
missing, and foreign references return PROJECT_THUMBNAIL_INVALID.
Asset browsing and project-to-asset relationships remain outside P1.
Validation and errors
Request schemas forbid unknown fields. Name, description, metadata size, enum values, UUID path parameters, limits, and cursor size are bounded before repository access. Database exceptions are handled by the global safe error envelope and are never returned directly.
Project-specific codes are:
PROJECT_NOT_FOUNDPROJECT_INVALID_NAMEPROJECT_INVALID_STATUSPROJECT_THUMBNAIL_INVALIDPROJECT_ALREADY_ARCHIVEDPROJECT_INVALID_CURSOR
Normal schema failures use the existing VALIDATION_ERROR envelope.
Audit and logging
The shared AuditService records:
project.createdproject.updatedproject.archived(PATCH status transition)project.deleted(DELETE requested; disposition isarchived)
Audit metadata contains only bounded field names and lifecycle disposition. Full project metadata, credentials, tokens, authorization headers, and secrets are not recorded. Structured operational logs contain the project ID, workspace ID, operation, duration, and result.
The existing request audit and rate limiter continue to wrap every project route.
Frontend activation
OpenAPI advertises:
GETandPOSTon/v1/projectsGET,PATCH, andDELETEon/v1/projects/{project_id}ProjectCreate,ProjectUpdate,ProjectResponse,ProjectListResponse, andProjectStatus
This satisfies the existing frontend's route-based project capability discovery. The frontend does not need a hardcoded project availability flag.
Migration operations and rollback
Production migrations remain externally applied; application startup never mutates PostgreSQL.
Schema readiness now fails closed if projects, audit_events, or required project indexes are
missing.
The repository has no destructive down-migration convention. If rollback is required before any dependent domain exists, operators should first disable project traffic, preserve/export project and audit data, then remove policies, triggers, indexes, and tables in dependency order through the normal reviewed database process. Never run that procedure against production without a verified backup.
Current limitations and future relationships
P1 intentionally has no project sharing, comments, versions, editor state, favorite/pin state,
templates execution, or child resource relationships. The projects.id key is stable and the
workspace ownership constraint is suitable for future composite/trigger-validated references.
Each future child domain must reuse its authoritative table, retain its own workspace ID where the
existing tenancy architecture requires it, and enforce that the child and project workspaces match.
No second asset, job, template, or membership system should be created.