Prima Get an API key

Module kit / Media, audio and design

Design (Canvas) CRUD and Versioning

media-audio-design/design-canvas-crud-and-versioning

Kind backend
Domain media-audio-design
Maturity prod
Reusable extract
Stack fastapi-0.118.2, sqlalchemy-2.0.43-sync, postgres-15-prod-14-test, pydantic-2.12.0, alembic-1.16.5, psycopg2-2.9.10
Depends on _shared/fastapi-project-layout, _shared/auth-dependency-pattern, _shared/workspace-scoping, _shared/alembic-migrations, _shared/test-layout
Frontends media-audio-design/ui/picasso-design-gallery
Reference implementation
  • alchemy_labs/backend/api/routers/design_router.py
  • alchemy_labs/backend/services/design/design_service.py
  • alchemy_labs/backend/models/design_models.py
  • alchemy_labs/backend/schemas/design_schemas.py
  • alchemy_labs/backend/migrations/versions/20260205_add_design_tables.py
Measured on alchemy_labs main 62ba4fc41009b454e1c033e5036c7e8577db2061 2026-09-12
Version 1

Purpose

Lets a workspace member create a Picasso canvas design (blank, or from an agency DesignTemplate with that template's brand_mappings baked into the copied Fabric.js canvas_data before the row is ever written), edit it in place or fork a new numbered version, archive or permanently delete it, and read a design's version history back out (design_service.py 34 to 462). The invariant the row shape is built to keep is "exactly one is_current_version=True row per lineage, chained backward through previous_version_id" (design_models.py 147 to 150, 166) -- but that invariant is only half-delivered: the backward walk to the lineage root in get_versions works, while the forward query that is supposed to collect every version off that root does not reconstruct chains longer than two rows (see Application logic and Gotchas). A rebuild that copies this file's get_versions verbatim inherits that gap.

When to use

Read it before touching DesignService.create_design, create_from_template, _apply_brand_to_canvas, update_design, delete_design, get_versions, or duplicate_design; before changing what create_version=true does on PUT /api/designs/{id}; when someone asks "why is my design missing from its own version history", "does creating a design from a template apply the client's brand colors", "what happens if I delete a design", or "why did updating with create_version not make a new version". Trigger phrases: "Picasso design", "canvas_data", "Fabric.js JSON", "brand_mappings", "create_version", "hard_delete", "is_current_version", "previous_version_id", "version chain".

Not this recipe: DesignTemplate CRUD itself (POST/GET/PUT/DELETE /api/designs/templates*, design_router.py 46 to 223, backed by DesignTemplateService in services/design/template_service.py, a sibling file not read for this recipe) -- this recipe only reads a template's canvas_data/brand_mappings/width/height off it, never writes one; POST /api/designs/{id}/export and /export/deliverable (design_router.py 502 to 579), both stubs that return a message rather than rendering anything server-side ("Export rendering should be done client-side with Fabric.js toDataURL()", line 539) or creating a real ProjectDeliverable ("Deliverable creation not yet implemented", line 578); Prima's own design-editing DSL (prima_design_router, DesignOperation/select_template/set_text/set_color/etc. in design_schemas.py 29 to 68, mounted separately in app_fastapi.py 975), which calls into this service but is a distinct request/response surface; DesignMedia upload (design_models.py 203 to 258), read here only as the table Design.media cascades onto during a hard delete; GET /api/designs/brand-assets and the _get_brand_assets helper's read of BrandEssence (design_router.py 586 to 655), read here only as the input _apply_brand_to_canvas consumes.

Stack assumptions

At 62ba4fc: fastapi==0.118.2, synchronous handlers on sqlalchemy.orm.Session (SQLAlchemy==2.0.43), pydantic==2.12.0 for every request/response schema, psycopg2-binary==2.9.10, alembic==1.16.5 (backend/requirements.txt 6, 32, 76, 80, 109). Postgres 15 in prod (docker-compose.prod.yml 9, postgres:15-alpine), Postgres 14 in the test harness (docker-compose.test.yml 6, postgres:14). Every endpoint is async def but every DB call inside it is the synchronous Session API (no await on any query); nothing in this file uses a thread pool or an async driver, matching S4's documented pattern for this codebase.

Foundations assumed: S4 (fastapi-project-layout) for the app and mount_router; S5 (auth-dependency-pattern) for get_current_user (api/dependencies.py 187 onward, JWT bearer then cookie then X-API-Key, 401 with WWW-Authenticate: Bearer on failure); S6 (workspace-scoping) for WorkspaceID (api/dependencies.py 779, Annotated[UUID, Depends(_get_workspace_id)]), whose fail modes this router inherits unmodified: X-Workspace-ID header re-validated against the caller (403 if the user lacks access, 400 if the header is not a UUID), else single-workspace inference, else 409 {"error": "no_workspace"} telling the client to call POST /api/workspaces/bootstrap, else 400 for an ambiguous multi-workspace caller with no header (api/dependencies.py 698 to 776); S8 (alembic-migrations) for the one migration under Deploy & ops. No environment variable gates this module (no PICASSO_*/DESIGN_* key in core/config.py; grep -rn "picasso" backend --include=*.py outside frontend/ and this router/service pair finds nothing), and the router mounts on LABS_SURFACES (app_fastapi.py 973 to 975, from core.surfaces import ... LABS_SURFACES at 139) alongside the rest of the authenticated product surface, guarded only by DESIGN_ROUTER_AVAILABLE, an import-success flag set by a try: from api.routers import design_router, prima_design_router / except ImportError around the module import (220 to 226) -- the same pattern this file uses for several other optional routers, not a deliberate on/off switch for Picasso.

Data model

designs, one row per version, versions chained by previous_version_id (models/design_models.py 120 to 200; migrated in one file, see Deploy & ops):

designs (
  id                   uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id              uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  workspace_id         uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
  template_id          uuid REFERENCES design_templates(id) ON DELETE SET NULL,
  project_id           uuid REFERENCES projects(id) ON DELETE SET NULL,
  name                 varchar(255) NOT NULL DEFAULT 'Untitled Design',
  description          text,
  width                integer NOT NULL DEFAULT 1080,
  height               integer NOT NULL DEFAULT 1080,
  canvas_data          jsonb NOT NULL DEFAULT '{}',        -- Fabric.js canvas JSON
  version_number       integer DEFAULT 1,
  is_current_version   boolean DEFAULT true,
  previous_version_id  uuid REFERENCES designs(id) ON DELETE SET NULL,
  thumbnail_url        text,
  exported_url         text,
  status               varchar(50) NOT NULL DEFAULT 'draft',
  created_at           timestamptz DEFAULT CURRENT_TIMESTAMP,
  updated_at           timestamptz DEFAULT CURRENT_TIMESTAMP
);
CHECK designs_status_check: status IN ('draft', 'published', 'archived')

Column-level note, not a data-model bug but a real mismatch between the ORM class and the migration that created its table: the SQLAlchemy model declares status = Column(Enum(DesignStatus), default=DesignStatus.draft) (design_models.py 157, a Python str, enum.Enum at lines 19 to 23), while the migration that actually created the column typed it sa.String(50) plus the designs_status_check CHECK, never sa.Enum(...) (20260205_add_design_tables.py, the status column and its check constraint). The three enum values agree, so reads and writes work identically either way at this commit; the drift only matters if something ever calls Base.metadata.create_all() against a fresh database, which would try to CREATE TYPE for a Postgres-native enum the migration never created.

design_templates, read-only from this recipe's side: canvas_data (Fabric.js JSON, the seed this recipe deep-copies), brand_mappings (jsonb, e.g. {"headline_color": "primary", "logo_slot": "#1A2B3C"}, a map of Fabric object name/id to a brand-asset key or a literal hex string), width, height, description (design_models.py 33 to 117).

design_media, FK'd design_id uuid NOT NULL REFERENCES designs(id) ON DELETE CASCADE (design_models.py 203 to 240; 20260205_add_design_tables.py, the design_media table). Out of this recipe's scope except for one effect: a hard delete of a Design row cascades onto its design_media rows both at the DB FK and via the ORM relationship (cascade="all, delete-orphan", design_models.py 167), deleting the metadata rows but not the underlying S3 objects the s3_key/file_url columns point at -- nothing in delete_design calls out to storage.

What is NOT stored: any Fabric.js canvas produced by a blank create_design call is literally canvas_data = {} when the caller sends none (DesignCreate.canvas_data defaults to Field(default_factory=dict), design_schemas.py 188, written through unmodified at design_service.py 60) -- not a valid empty Fabric.js document ({"version": ..., "objects": []}), just an empty JSON object; nothing in this recipe fills that in, the shape is left to the frontend editor.

API surface

RouteAuthTenancyStatus codesNotes
POST /api/designs (design_router.py 231 to 256)get_current_userWorkspaceID (required)201; 400 on ANY exception from create_design (except Exception as e, 251 to 256, string-interpolated into the detail)Blank design, DesignCreate body; no brand application on this path even if canvas_data is supplied
POST /api/designs/from-template (259 to 291)get_current_userWorkspaceID201; 404 "Template not found" if the template lookup misses; no router-level try/except, unlike the blank-create route, so any other exception here is an unhandled 500DesignCreateFromTemplate body (template_id, optional name/project_id, apply_brand: bool = True); when apply_brand is true, _get_brand_assets is called before the service method, and its result is passed in as brand_assets
GET /api/designs (294 to 322)get_current_userWorkspaceID200project_id/status/search filters, limit 1 to 100 default 50, offset; returns only is_current_version=True rows; each item serialized with to_dict(include_canvas=False), so canvas_data is never in the list payload
GET /api/designs/brand-assets (325 to 337)get_current_usernone (keyed by current_user, not by workspace)200Calls _get_brand_assets, out of this recipe's scope except as the input to brand application
GET /api/designs/{design_id} (340 to 366)get_current_userWorkspaceID200; 404 "Design not found"Full row including canvas_data
PUT /api/designs/{design_id} (369 to 399)get_current_userWorkspaceID200; 404 "Design not found or unauthorized"DesignUpdate body; create_version: bool = Query(False) selects the branch (see Application logic)
DELETE /api/designs/{design_id} (402 to 429)get_current_userWorkspaceID204; 404 "Design not found or unauthorized"hard_delete: bool = Query(False); default is archive
GET /api/designs/{design_id}/versions (432 to 466)get_current_userWorkspaceID200; 404 "Design not found" (raised when get_versions returns an empty list, which is also what a design that exists but has no reachable versions would return, so a design that IS found but somehow yields zero rows from the version query is indistinguishable from a 404 at this endpoint)Response is hand-built per row (id, version_number, is_current_version, previous_version_id, created_at, updated_at), not validated against the imported DesignVersionResponse schema (design_schemas.py 250 to 261, which also carries an optional canvas_data -- never populated here)
POST /api/designs/{design_id}/duplicate (469 to 495)get_current_userWorkspaceID200; 404 "Design not found"new_name optional query param; starts a brand-new lineage, previous_version_id is not set

Application logic

create_design (design_service.py 34 to 71), in commit order: build a Design row directly from DesignCreate fields, canvas_data defaulting to {}, version_number=1, is_current_version=True, status=DesignStatus.draft; add + commit + refresh. No brand application on this path.

create_from_template (73 to 134): load the DesignTemplate scoped to workspace_id and is_archived == False (93 to 97) -- a template from another workspace, or an archived one, is a 404 the same as a missing id, the router cannot tell the two apart; copy.deepcopy(template.canvas_data or {}) (104); iff data.apply_brand AND the caller passed a non-empty brand_assets dict AND template.brand_mappings is non-empty, call _apply_brand_to_canvas(canvas_data, template.brand_mappings, brand_assets) (107 to 112) -- any one of those three being falsy silently skips brand application, there is no error or flag in the response saying it did not run; build the Design row (name defaults to f"Design from {template.name}" when not given, width/height/description copied from the template, not from the request); add + commit + refresh.

_apply_brand_to_canvas (136 to 188), per object in canvas_data["objects"]: look up brand_mappings.get(obj["name"]) or brand_mappings.get(obj["id"]) (name checked first); if no mapping, skip. Then, in this exact order:

  1. mapping in ("primary", "secondary", "accent") -> take brand_assets["colors"][mapping][0] (first color in that list; a mapping to a color category with an empty list is a silent no-op) and set obj["fill"] -- but only when obj["type"] is "i-text", "text", "rect", "circle", or "polygon" (168 to 175); an "image", "group", "path", "line", or any other Fabric object type is left untouched even if it carries a matching name.
  2. else mapping.startswith("#") -> set obj["fill"] to that literal hex string, unconditional on object type (178 to 179).
  3. else mapping in ("headings", "body") -> set obj["fontFamily"] from brand_assets["typography"][mapping], only for "i-text"/"text" objects (182 to 185).

Two measured gaps in that switch, not inferred: _get_brand_assets also returns a "neutral" color list (design_router.py 623 to 627) and an "accent" typography font (629 to 633), but _apply_brand_to_canvas's branch list only ever checks ("primary", "secondary", "accent") for colors and ("headings", "body") for fonts -- a brand_mappings value of "neutral" matches none of the three branches and is a silent no-op, and a value of "accent" is always claimed by the color branch (step 1) before the font branch is ever reached, so an accent font can never be applied through this mechanism, only an accent color.

update_design (268 to 334): loads the design scoped to user_id (+workspace_id when given); branches on create_version and update_data.canvas_data (293), not on the create_version query flag alone -- calling PUT ...?create_version=true with a body that omits canvas_data (or sends canvas_data: null) silently falls through to the update-in-place branch below, with no error and no indication in the response that a version was not created. Create-version branch (294 to 319): build a new Design row copying user_id/workspace_id/template_id from the existing row, name/description/width/height/status from the update payload or falling back to the existing row's value, canvas_data from the payload verbatim, version_number = design.version_number + 1, is_current_version=True, previous_version_id = design.id; then set design.is_current_version = False on the OLD row; add the new row; one commit. Update-in-place branch (320 to 334): update_data.model_dump(exclude_unset=True), coercing status through DesignStatus(value) when present, setattr per field, bump updated_at, commit.

delete_design (336 to 368): hard_delete=True -> db.delete(design) (cascades to design_media per the FK/ORM relationship above); else sets status = DesignStatus.archived and bumps updated_at. Either way, one commit. A soft-deleted (archived) design is not excluded from get_design/get_versions by status -- only list_designs can filter it out, and only if the caller explicitly passes status=archived or status=draft/published; with no status filter, archived designs still appear in the list.

get_versions (370 to 417), the version-chain walk: load the design named by design_id; walk previous_version_id backward, one get_design query per hop, until a row with no previous_version_id is reached -- call this current (392 to 398). Then query Design.user_id == user_id AND (Design.id == current.id OR Design.previous_version_id == current.id), optionally AND Design.workspace_id == workspace_id, ordered by version_number descending (401 to 413). This is the gap named in Purpose, measured directly from the code, not inferred: that second query only matches rows whose previous_version_id equals the ROOT's id -- i.e. the root itself plus exactly its one direct child (version 2 of the lineage). For a lineage of v1 -> v2 -> v3 (v3.previous_version_id == v2.id, not v1.id), calling get_versions on any id in that chain returns [v2, v1] and never v3. The function's own trailing comment says as much: "This is a simplified approach - for deep version chains, we might need a recursive CTE" (415 to 416) -- the limitation is acknowledged in the source, not hidden.

duplicate_design (419 to 462): loads the source design; builds a brand-new Design row (copy.deepcopy(source.canvas_data), name defaults to f"{source.name} (Copy)", version_number=1, is_current_version=True, status=DesignStatus.draft) with previous_version_id left unset -- a duplicate starts a fresh, independent lineage rather than extending the source's version chain.

Invariants

  • A blank design and a from-template design both land with version_number=1, is_current_version=True, previous_version_id=NULL (create_design 51 to 64; create_from_template 114 to 127) -- no automated test asserts this (see Tests).
  • Brand assets are only ever applied on the from-template path, and only when apply_brand, brand_assets, and template.brand_mappings are all truthy (create_from_template 107); a blank create_design call never applies brand mappings even if the caller supplies matching canvas_data.
  • create_version=true on PUT /api/designs/{id} only takes effect when the request body also carries canvas_data; otherwise the update is applied in place to the existing row (update_design 293).
  • Exactly one row per lineage carries is_current_version=True after an in-place update (unchanged) or a version fork (the old row is flipped to False in the same transaction as the new row's insert, 293 to 319) -- unverified by any test at this commit.
  • get_versions reconstructs at most two rows of a lineage longer than two versions (root + one child), a measured limitation the source code comments on itself (design_service.py 415 to 416).

Frontend

The module that would render this, by id:

media-audio-design/ui/picasso-design-gallery   the design list/creation surface these endpoints back;
                                                at 62ba4fc, frontend/src/components/picasso/PicassoEditor.tsx,
                                                frontend/src/components/picasso/hooks/useDesignOperations.ts and
                                                frontend/src/services/picassoService.ts are the real callers of
                                                /api/designs* (grep -rl "/api/designs" frontend/src), not yet
                                                written as a prima-modules frontend recipe

Minimum admin/debug surface: none dedicated exists at this commit (no admin route lists designs across users/workspaces); the closest thing a human has is GET /api/designs?workspace_id=... scoped to their own session, and a direct query against designs for anything wider.

Security & tenancy

Auth dependency: get_current_user (S5), JWT bearer/cookie/API-key, 401 on failure. Tenancy: WorkspaceID (S6) is required on every route in this router except GET /api/designs/brand-assets, which is scoped by current_user.id only, not by workspace -- _get_brand_assets queries BrandEssence.user_id == user.id (design_router.py 593 to 595), so a user who belongs to two workspaces gets the same brand assets applied in both. get_design/update_design/delete_design/get_versions/duplicate_design all take workspace_id as an Optional[UUID] = None parameter at the service layer (design_service.py 190 to 462) and only add the Design.workspace_id == filter when it is truthy; every router call site supplies it from the required WorkspaceID dependency, so in practice it is never omitted through this router, but the service method itself does not enforce that on its own.

Fail direction: workspace access closed (S6's 403/400/409 fail modes, inherited unmodified); a design lookup that misses on id+user_id(+workspace_id) is a 404, not distinguishing "does not exist" from "exists in another tenant" from "exists but is archived". create_design's router wraps its whole body in except Exception as e: raise HTTPException(400, f"Failed to create design: {str(e)}") (251 to 256) -- any exception, including a DB constraint violation or a bug, is reported to the caller as a 400 with the exception's string interpolated into the response body; create_design_from_template has no equivalent catch, so the same class of failure on that path is an unhandled 500 instead.

Secrets and PII: canvas_data is arbitrary Fabric.js JSON supplied by the caller and stored verbatim in jsonb, no schema validation beyond "is a JSON object" (Dict[str, Any] in every relevant Pydantic schema); nothing in this recipe inspects it for size or content before persisting it. Rate limits: none dedicated to this router (not measured; no rate-limit dependency appears in the route decorators or mount_router call at app_fastapi.py 974 to 975).

Tests

No automated tests exist for this module at 62ba4fc. grep -rln "DesignService|design_service|from services.design|design_models|design_schemas" backend/tests (excluding __pycache__) returns nothing; the only test file with "design" in its name, backend/tests/v2/coverage/test_forms_g6_templateservice_crud_design_normalization_service_layer_db_backed_.py, is entirely about services/forms/template_service.py's _normalize_design_to_style_config (a form-builder style-config mapper), unrelated to design_router.py/design_service.py/design_models.py. This is a genuine gap, not a measurement I could round up: none of the invariants listed above, and none of the branch conditions in create_from_template, update_design, or get_versions, have a known-answer test backing them at this commit. A rebuild following S9 (test-layout)'s pytest conventions would be the first test coverage this module has had.

Deploy & ops

Config keys: none (no dedicated environment variable gates this module; core/config.py has no PICASSO_*/DESIGN_* entry, confirmed by grep -n "PICASSO\|DESIGN_" backend/core/config.py returning nothing at this commit); the only gate is the import-success flag DESIGN_ROUTER_AVAILABLE described under Stack assumptions. Migration: one file, backend/migrations/versions/20260205_add_design_tables.py (revision = '20260205_design_tables', down_revision = '20260124_li_fat_id'), creates design_templates, designs, design_media and their indexes/check constraints in a single upgrade(); no later migration alters any of the three tables (grep -rl "'designs'" backend/migrations/versions/ returns only this one file). Scheduler: none, this module has no background job. Argus-style check: GET /api/designs with a valid session and workspace header returns 200 and a JSON array (empty on a fresh workspace); no dedicated health probe exists beyond that. Rollback: designs are never deleted by anything other than an explicit hard_delete=true call from a user, so rolling back a code deploy does not need to unwind data on its own.

Gotchas

  • 2026-09-12 (measured at 62ba4fc, design_service.py 392 to 417): get_versions walks backward to the lineage root correctly but the forward collection query (Design.previous_version_id == current.id) only ever matches the root's direct child, so a lineage of three or more versions returns just the first two rows. The function's own comment flags this ("we might need a recursive CTE"); nothing has fixed it as of this commit.
  • 2026-09-12 (measured at 62ba4fc, design_service.py 107, 168 to 185): brand-mapping application only runs on create_from_template, never on a blank create_design; and within _apply_brand_to_canvas, a brand_mappings value of "neutral" matches none of the color/font branches (silent no-op), and a value of "accent" is always consumed by the color branch before the font branch runs, so the "accent" typography font _get_brand_assets returns can never actually reach a canvas object through this mechanism.
  • 2026-09-12 (measured at 62ba4fc, design_router.py 251 to 256 vs 259 to 291): create_design catches every exception and reports it as a 400; create_design_from_template, right next to it, has no such catch, so the same underlying failure (e.g. a DB constraint violation) is a 400 on one route and an unhandled 500 on the other.
  • 2026-09-12 (measured at 62ba4fc, design_models.py 157 vs 20260205_add_design_tables.py): the ORM column is typed Enum(DesignStatus) but the migration that created it typed the column sa.String(50) with a CHECK constraint instead of a native Postgres enum type. Harmless at this commit (values agree), but Base.metadata.create_all() on a fresh database would not reproduce the migration's schema exactly.
  • 2026-09-12 (measured at 62ba4fc): zero automated tests cover this module (see Tests). Every behavior documented in Application logic and Invariants above was verified by reading the source, not by a passing test.

Reference implementation

Paths relative to the repo root; all at main 62ba4fc41009b454e1c033e5036c7e8577db2061 unless noted.

  • alchemy_labs/backend/api/routers/design_router.py (655 lines): design endpoints (231 to 495); export stubs (498 to 579, out of scope); _get_brand_assets (586 to 655, out of scope except as brand-application input).
  • alchemy_labs/backend/services/design/design_service.py (521 lines, whole file in scope): create_design (34 to 71); create_from_template (73 to 134); _apply_brand_to_canvas (136 to 188); get_design (190 to 215); list_designs (217 to 266); update_design (268 to 334); delete_design (336 to 368); get_versions (370 to 417); duplicate_design (419 to 462); set_thumbnail/set_exported_url (464 to 520, not reached from any route in this router at this commit -- grep -rn "set_thumbnail\|set_exported_url" backend/api finds no caller).
  • alchemy_labs/backend/models/design_models.py (258 lines, whole file in scope): DesignStatus/MediaType enums (19 to 30); DesignTemplate (33 to 117, read-only from this recipe); Design (120 to 200); DesignMedia (203 to 258, out of scope except its cascade).
  • alchemy_labs/backend/schemas/design_schemas.py (392 lines): DesignBase/DesignCreate/DesignCreateFromTemplate/DesignUpdate/DesignResponse/DesignListResponse/DesignVersionResponse (176 to 261); BrandColorsResponse/BrandTypographyResponse/BrandAssetsResponse (335 to 356, the shape _apply_brand_to_canvas consumes).
  • alchemy_labs/backend/migrations/versions/20260205_add_design_tables.py (whole file in scope for the three tables and their constraints).
  • alchemy_labs/backend/app_fastapi.py: router mount (974 to 975); LABS_SURFACES import (139).
  • alchemy_labs/backend/api/dependencies.py: get_current_user (187 onward); _get_workspace_id/WorkspaceID (around 690 to 779).
  • No test file references this module (see Tests); there is nothing further to cite there.

Deviations of the reference from an ideal implementation, all dated 2026-09-12: get_versions does not reconstruct lineages longer than two rows; brand-mapping application has two dead branches (neutral color, accent font); the two create endpoints disagree on whether a generic exception is a 400 or a 500; the status column's ORM type and its migration's actual column type disagree; and the whole module ships with no automated test coverage.

Acceptance checks

Run from alchemy_labs/backend at 62ba4fc (or an app built to this recipe). No pytest command is given here because none exists for this module (Tests); these are read-only structural checks a verifier can run without booting a database.

  1. grep -n "def get_versions" -A 50 services/design/design_service.py | grep -c "previous_version_id == current.id" prints 1 -- confirms the single forward-match condition described in Application logic and Gotchas is still exactly that one equality, not a recursive query.
  2. grep -n "except Exception as e" api/routers/design_router.py prints exactly two lines, 69: (inside create_design_template, out of this recipe's scope) and 251: (inside create_design); sed -n '259,291p' api/routers/design_router.py | grep -c "except Exception" prints 0, confirming create_design_from_template has no such catch.
  3. grep -n "class DesignStatus" -A 5 models/design_models.py shows the three values draft, published, archived; grep -n "designs_status_check" -A 3 migrations/versions/20260205_add_design_tables.py shows the same three values in the CHECK constraint's "status IN ('draft', 'published', 'archived')" string, two lines below the constraint name.
  4. grep -rlnE "DesignService|design_service|design_router" tests/ 2>/dev/null | grep -v __pycache__ prints nothing, confirming the Tests section's claim of zero coverage at this commit.
  5. grep -n "DESIGN_ROUTER_AVAILABLE\|mount_router(app, design_router.router" app_fastapi.py shows the flag set at 223/226 from a try/except ImportError and the mount at 974 guarded by if DESIGN_ROUTER_AVAILABLE: at 973, confirming the import-success guard (not a business flag) claimed under Stack assumptions.

Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14. The recipe file is the source; this page is a rendering of it.