Skip to content

Design & artwork

Crates: platyn-design, platyn-storage · Migration: 0005_design.sql · Status: Schema built API planned

The tables exist and are applied. The HTTP surface and the rendering jobs are planned.

Art is where decorating jobs go wrong. Not the quote, not the shipping — the art. A customer sends a 400×400 JPEG of a logo and expects it on the front of a hoodie. A designer cleans it up. A proof goes out. Someone approves it. Two weeks later, after 300 shirts are printed, the customer says the blue is wrong.

The job of this domain is to make that conversation short: here is exactly what was approved, here is who approved it, here is the timestamp, and here is the IP it came from.

erDiagram
    TENANTS   ||--o{ DESIGNS : owns
    COMPANIES ||--o{ DESIGNS : "logo library"
    DEALS     ||--o{ DESIGNS : "art for"
    LINE_ITEMS ||--o{ DESIGNS : "art for"
    DESIGNS   ||--o{ DESIGN_VERSIONS : "revised as"
    DESIGN_VERSIONS ||--o{ DESIGN_ASSETS : "files"
    DESIGN_VERSIONS ||--o{ IMPRINTS : "placed by"
    DESIGN_VERSIONS ||--o{ APPROVALS : "signed off"
    APPROVALS ||--o{ APPROVAL_COMMENTS : "discussed in"
    LINE_ITEMS ||--o{ IMPRINTS : "decorated with"
    CUSTOMERS ||--o{ APPROVALS : decides

The logical design — “Riverside Brewing 2026 logo”. Its identity survives revisions, which is what makes a reorder cheap.

CREATE TYPE app.design_status AS ENUM
('concept','in_progress','proof_ready','in_review','revising',
'approved','production_ready','archived');
ColumnTypeNotes
id, tenant_idUUIDRLS key.
company_idUUID nullableON DELETE SET NULL. Library art reusable across that company’s jobs.
deal_idUUID nullableON DELETE SET NULL.
line_item_idUUID nullableON DELETE SET NULL. Art tied to one specific line.
nameTEXT NOT NULL
codeTEXTThe shop’s own design number.
statusapp.design_statusDefault concept.
designer_idUUIDapp.users
created_at, updated_atTIMESTAMPTZ

All three parent links are SET NULL rather than CASCADE: deleting a deal must not destroy the artwork, because the art usually outlives the job that paid for it.

Immutable revisions. “Approve v3” is unambiguous; “approve the design” is not.

ColumnType
id, tenant_id, design_idUUID
version_noINTEGERUNIQUE (design_id, version_no)
notesTEXT — what changed and why
created_by, created_at
superseded_atTIMESTAMPTZ
CREATE TYPE app.asset_kind AS ENUM
('mockup','production_art','separation','proof','logo','spec_sheet','other');
ColumnTypeNotes
id, tenant_id, design_version_idUUID
kindapp.asset_kindDefault mockup.
storage_keyTEXT NOT NULLAn S3 object key. Never a host path, never a public URL.
filename, mime_typeTEXT NOT NULL
byte_sizeBIGINT
width_px, height_pxINTEGERDrives the “this is 72 DPI” warning.
checksum_sha256BYTEADetects a truncated upload.
is_primaryBOOLEAN
positionINTEGER
statusTEXTpendingready. See below.
uploaded_by, created_at, deleted_atSoft delete.
CREATE UNIQUE INDEX design_assets_one_primary
ON app.design_assets (design_version_id)
WHERE is_primary AND deleted_at IS NULL;

storage_key being an S3 key is a deliberate correction. The predecessor wrote to a host mount at /cmp-share, which pinned the entire application to one machine forever. Bytes never pass through Bun or Rust: uploads and downloads use presigned URLs, so the browser talks to S3 directly and nothing proxies a 200 MB Illustrator file.

Where art physically goes on the garment, with the parameters that drive cost.

ColumnTypeNotes
id, tenant_idUUID
line_item_idUUID NOT NULLON DELETE CASCADE. An imprint belongs to a line.
design_version_idUUID nullableThe exact art being printed.
locationTEXT NOT NULLfull_front, left_chest, back_yoke, sleeve_left.
decoration_method_idSMALLINT
color_countSMALLINTScreen print: the primary price driver.
ink_colorsTEXT[]Pantone references.
stitch_countINTEGEREmbroidery: the equivalent driver.
width_in, height_inNUMERIC(6,2)
position, notes
CREATE TYPE app.approval_status AS ENUM ('pending','approved','rejected','expired','cancelled');
CREATE TYPE app.author_kind AS ENUM ('staff','customer','system');
ColumnTypeNotes
id, tenant_idUUID
design_version_idUUID NOT NULLThe exact revision. ON DELETE CASCADE.
statusapp.approval_statusDefault pending.
requested_by, requested_at
sent_to_customer_idUUIDapp.customers
token_hashBYTEA UNIQUEScoped, account-less approval link. Hash only.
expires_at, decided_atTIMESTAMPTZ
decided_by_customer_idUUIDThe customer decided.
decided_by_user_idUUIDOr a rep decided on their behalf.
decision_ipINETEvidence.
decision_user_agent, reasonTEXT

The approval token is the mechanism that lets a customer sign off without an account. Only the hash is stored; the raw token exists solely in the email that was sent. It authorizes exactly one resource and a very small verb set — it cannot list deals and it cannot see pricing.

ColumnTypeNotes
id, tenant_idUUID
approval_idUUID nullable
design_version_idUUID nullable
author_kindapp.author_kindstaff · customer · system
author_user_id, author_customer_idUUIDWhichever applies.
bodyTEXT NOT NULL
created_atTIMESTAMPTZ
CHECK (num_nonnulls(approval_id, design_version_id) >= 1)

A comment must attach to something — an approval round or a version — enforced by the database rather than by hope. system comments are written by the application (“proof sent”, “version superseded”), so one thread shows both what people said and what the system did.

flowchart LR
    I["concept<br/>customer file arrives"] --> D["in_progress<br/>clean up, separate"]
    D --> PR["proof_ready<br/>mockup rendered"]
    PR --> R["in_review<br/>approval sent"]
    R -->|approved| A["approved"]
    R -->|rejected| RV["revising<br/>→ new version"]
    RV --> D
    A --> P["production_ready<br/>separations + specs"]
    P --> PRESS["to press"]

    style I fill:#00A3D9,stroke:#007AA6,color:#14131A
    style R fill:#E4006C,stroke:#B00054,color:#ffffff
    style P fill:#FF5B23,stroke:#D9410F,color:#14131A
  1. Concept. The customer’s file lands via presigned upload as a logo asset, row pending.
  2. Assess. Once the storage event flips it to ready, a job extracts dimensions and checksum. A 72-DPI raster is not going on a hoodie front, and the system flags it before a designer spends an hour discovering it.
  3. In progress. Cleanup, vectorisation, separation or digitising. Each save is a new design_version.
  4. Proof ready. A mockup asset — the art composited onto the actual garment colour, at the actual print size — marked is_primary.
  5. In review. An app.approvals row is created with a hashed token and an expires_at, and the link is emailed to sent_to_customer_id.
  6. Decided. Approve or reject, with the IP, user agent, and timestamp recorded. A rejection moves the design to revising and the next save becomes version n+1.
  7. Production ready. Separations, ink colours, and placement measurements — everything the press needs.

All enqueued transactionally through app.jobs, so a design write and the job that processes it either both happen or neither does — ADR-06.

kindDoes
design.asset_readyHandle the storage event, extract metadata, flip pendingready
design.thumbnailWeb-sized previews
design.mockupComposite art onto the garment colour
design.separateSplit spot colours into printable separations
approval.sendRender the email, mint the token, deliver
approval.expireSweep approvals past expires_at into expired
MethodPathPermission
GET/v1/designsdesign.read
GET/v1/designs/{id}design.read
POST/v1/designsdesign.write
POST/v1/designs/{id}/versionsdesign.write
POST/v1/design-versions/{id}/upload-urldesign.write — returns a presigned PUT
GET/v1/design-assets/{id}/urldesign.read — returns a presigned GET
POST/v1/line-items/{id}/imprintsdesign.write
POST/v1/design-versions/{id}/approvalsdesign.write
GET/v1/approvals/{id}design.read
POST/v1/approvals/{id}/decidedesign.approveor a valid approval token
POST/v1/approvals/{id}/commentsdesign.read — or a valid approval token

design.approve is held only by owner and admin. A designer can produce art and send a proof; committing ink to garments is a separate authority.

The token-authenticated paths are the only routes in the entire API reachable by a principal that is not a session. They resolve a single approvals.token_hash, authorize exactly that row, and expose nothing else.

  • Automatic spot-colour separations, with manual override. The hard, valuable part.
  • A garment mockup library — real photographs per style colour, so a proof shows the actual heather rather than a flat swatch.
  • Ink colour matching against Pantone, with a shop-specific mix book. imprints.ink_colors is already TEXT[] and waiting for it.
  • Reusable screen tracking, so a repeat order can skip the setup fee — which requires knowing the screen still exists.
  • A customer art portal, extending the approval token to uploads.