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.