Spaces:
Running
Running
File size: 8,711 Bytes
1b2323a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | # 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:
```text
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 retrieve
- `projects:create` — create
- `projects:update` — patch
- `projects: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) or `archived`
- `search`: case-insensitive name/description search over the loaded workspace
- `limit`: 1–100, default 50
- `cursor`: 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_FOUND`
- `PROJECT_INVALID_NAME`
- `PROJECT_INVALID_STATUS`
- `PROJECT_THUMBNAIL_INVALID`
- `PROJECT_ALREADY_ARCHIVED`
- `PROJECT_INVALID_CURSOR`
Normal schema failures use the existing `VALIDATION_ERROR` envelope.
## Audit and logging
The shared `AuditService` records:
- `project.created`
- `project.updated`
- `project.archived` (PATCH status transition)
- `project.deleted` (DELETE requested; disposition is `archived`)
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:
- `GET` and `POST` on `/v1/projects`
- `GET`, `PATCH`, and `DELETE` on `/v1/projects/{project_id}`
- `ProjectCreate`, `ProjectUpdate`, `ProjectResponse`, `ProjectListResponse`, and `ProjectStatus`
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.
|