At a Glance
Owner: Owning module (PR, PO, GRN, …) · Table:tb_attachment· Used by: every module with file uploads · Generic file-metadata catalogue — S3-backed, polymorphic linkage from owning module.
The attachment entity is the shared binary metadata catalogue for every module that stores files. Quotations on a PR, vendor confirmations on a PO, signed delivery dockets on a GRN, count sheets on a physical count, photos on a spot check — all land in tb_attachment and link back through application-side polymorphism (no in-schema FK from this table). Each row carries the S3 token + folder, original file metadata (name, ext, type, size), a public/signed URL, free-form info, and a doc_version integer for regenerated documents (e.g. re-rendered PDF).
The entity is intentionally generic — no document_type discriminator. The convention is that the owning table holds the FK columns to attachment rows.
Maintained by the owning module's upload flow. Read by the owning module's detail screens.
On read, the stored URL is not trusted — it is re-resolved. For every attachment returned, the gateway resolves a fresh presigned MinIO URL (1-hour TTL) from the internal fileToken; if resolution fails it falls back to a relative gateway route /api/{bu_code}/documents/{fileToken}/download. The internal fileToken is then stripped from the response — callers only ever see fileUrl.
The attachment shape exposed in comment responses:
| Field | Type | Notes |
|---|---|---|
fileName |
string | Original file name |
fileUrl |
string (optional) | Presigned URL, or relative fallback route; refilled per request |
contentType |
string | MIME type (image/jpeg, image/png, image/webp, image/gif, application/pdf) |
size |
number (optional) | Bytes |
Upload limits on POST :id/attachment: 1–10 files, ≤ 10 MB each, MIME restricted to the five types above. Comment responses additionally strip internal author fields (user_id, username, firstname, middlename, lastname) — the resolved author is exposed via the enriched audit object instead.
Note — two unrelated
doc_versionfields.tb_attachment.doc_versionis a re-render counter for regenerated documents (e.g. a re-printed PDF); it is not the optimistic-concurrencydoc_versioncarried by transactional documents. See system-config/doc-version.
| Task | Where | Notes |
|---|---|---|
| Attach a file to a document | Document detail → Attachments tab → Upload | Writes tb_attachment row + FK on owning table |
| Download attachment | Click filename | Gateway re-resolves a presigned URL (1-hour TTL) from the file token each request; falls back to /api/{bu_code}/documents/{fileToken}/download |
| Remove an attachment | Attachments tab → Delete | Soft-deletes the row; S3 blob reaped by GC job |
| Re-render a document PDF | Owning module print action | Increments doc_version; older versions retained for audit |
| Replace an uploaded file | Soft-delete old + upload new | s3_token unique among non-deleted rows |
| Symptom | Cause | Action |
|---|---|---|
| "Duplicate s3_token" | Existing non-deleted row | Soft-delete or use a different token |
| Broken file_url | Signed URL expired | Re-resolve via S3 service rather than caching |
| File missing in S3 but row exists | GC mismatch / external deletion | Orphan; reaped by retention job |
| MIME mismatch | file_type taken from client, not re-verified |
Do NOT rely on file_type/file_ext for security |
deleted_at hides from UI but does not remove the S3 object — GC reaps after retention.doc_version increment. Owning modules bump when re-rendering (e.g. PR PDF after stage advance); older versions stay downloadable from activity history.Source: tenant schema.
tb_attachment| Field | Prisma Type | Nullable | Description |
|---|---|---|---|
id |
String @db.Uuid |
No | Primary key. |
s3_token |
String? @db.VarChar(255) |
Yes | Opaque S3 object key. Unique among non-deleted rows. |
s3_folder |
String? @db.VarChar(255) |
Yes | Logical folder / bucket prefix. |
file_name |
String? @db.VarChar(255) |
Yes | Original filename. |
file_ext |
String? @db.VarChar(255) |
Yes | Extension (pdf, xlsx, jpg, …). |
file_type |
String? @db.VarChar(255) |
Yes | MIME type. |
file_size |
BigInt? @db.BigInt |
Yes | Bytes. |
file_url |
String? @db.VarChar(255) |
Yes | Resolved URL (signed or public). |
info |
Json? @db.Json |
Yes | Free-form metadata. |
doc_version |
Int @db.Integer |
No | Default 0. Incremented on re-render. |
| Audit columns | — | Yes | created_*, updated_*, deleted_*. |
Constraints: @@unique([s3_token, deleted_at]) map attachment_s3_token_u. @@index([s3_token]). No FK columns — linkage on owning table.
s3_token unique among non-deleted; constraint includes deleted_at so re-upload after soft-delete is allowed.doc_version increment. Owning modules bump on re-render; older versions retained.file_url across sessions.upload / download logged with entity_type = 'attachment'.../carmen-turborepo-backend-v2/packages/prisma-shared-schema-tenant/prisma/schema.prisma — tb_attachment (lines ~4427-4449)..../purchase-request/[id]/). Shared attachment service handles blob upload.