-
Notifications
You must be signed in to change notification settings - Fork 3
ADR 015 Paperless ngx Integration Architecture
Accepted
Cornerstone needs to integrate with Paperless-ngx for document management. Users want to link documents stored in Paperless-ngx to work items, household items, and vendor invoices. The application must browse, search, and display document metadata and thumbnails without storing documents locally.
Several architectural decisions need to be made:
- Communication pattern: Should the browser communicate directly with Paperless-ngx, or should Cornerstone's server proxy the requests?
-
Document linking schema: Should we use a single polymorphic
document_linkstable with anentity_typediscriminator, or separate junction tables per entity type (e.g.,work_item_documents,household_item_documents,invoice_documents)? - Caching strategy: Paperless-ngx document metadata and thumbnails are fetched on demand -- should we cache them, and if so, where?
- Configuration: How should the Paperless-ngx connection be configured?
All Paperless-ngx API requests are proxied through Cornerstone's Fastify server. The browser never communicates directly with Paperless-ngx.
Alternatives considered:
- Direct client-to-Paperless-ngx: The browser would need the Paperless-ngx API token, exposing it in client-side JavaScript. This is a security risk. It also requires CORS configuration on Paperless-ngx and exposes the Paperless-ngx URL to the browser, which may not be routable from the client network.
-
Server proxy (chosen): The Fastify server holds the Paperless-ngx API token securely. The client only talks to Cornerstone's
/api/paperless/*endpoints. This centralizes authentication, allows rate limiting, and simplifies CORS.
Rationale: Security is paramount. The API token grants full access to all documents in Paperless-ngx. Keeping it server-side follows the principle of least privilege. The proxy also allows Cornerstone to transform Paperless-ngx responses into a stable contract that won't break if the upstream API changes.
We use a single document_links table with an entity_type discriminator column rather than separate junction tables per entity type.
Alternatives considered:
-
Separate junction tables (
work_item_documents,household_item_documents,invoice_documents): Each table has a clear foreign key to its parent entity. This provides strong referential integrity via SQLite foreign keys. However, it requires three nearly identical tables with duplicate DDL, duplicate Drizzle schema definitions, and largely duplicate CRUD routes. -
Single polymorphic table (chosen): One
document_linkstable with columnsentity_type(enum:work_item,household_item,invoice) andentity_id(TEXT). This centralizes all document linking logic, reduces schema duplication, and makes it easy to add new entity types in the future.
Trade-off: The polymorphic approach cannot use SQLite foreign key constraints on entity_id because it references different tables depending on entity_type. Referential integrity for the entity side is enforced at the application layer (check that the referenced entity exists on insert, and cascade-delete links when an entity is deleted). The paperless_document_id column has no foreign key either (it references an external system). This is acceptable because:
- Document links are lightweight references, not critical business data
- Orphaned links (pointing to deleted entities) are harmless and can be cleaned up lazily
- The application layer already validates entity existence on link creation
- A composite unique constraint on
(entity_type, entity_id, paperless_document_id)prevents duplicate links
For the initial implementation, there is no caching layer. Each proxy request to Paperless-ngx results in a live upstream call.
Alternatives considered:
- In-memory LRU cache: Cache document metadata and thumbnails in a Node.js Map with TTL. This reduces latency and upstream load.
- SQLite cache table: Store fetched metadata in a local table with expiry timestamps.
- No cache (chosen for Phase 1): The simplest approach. With fewer than 5 users, the request volume to Paperless-ngx is minimal. The Paperless-ngx instance is typically on the same network (or even the same machine), so latency is low.
Future extensibility: If performance becomes a concern, an in-memory LRU cache with a configurable TTL (e.g., 5 minutes for metadata, 1 hour for thumbnails) can be added as a Fastify plugin without changing the API contract. The proxy endpoints would check the cache before calling upstream. This is noted but not built.
Paperless-ngx connection is configured via two environment variables:
| Variable | Default | Description |
|---|---|---|
PAPERLESS_URL |
(none) | Base URL of the Paperless-ngx instance (e.g., http://paperless:8000) |
PAPERLESS_API_TOKEN |
(none) | API authentication token for Paperless-ngx |
The integration is enabled when both variables are set. If either is missing, all /api/paperless/* endpoints return 503 SERVICE_UNAVAILABLE with error code PAPERLESS_NOT_CONFIGURED.
Rationale: Environment variables are the established configuration pattern in Cornerstone (consistent with OIDC configuration). The API token is sensitive and should never be stored in the database or exposed to the frontend. The paperlessEnabled flag is derived at startup and made available via fastify.config.
Cornerstone targets Paperless-ngx API version 5 (supported since Paperless-ngx 2.x). All proxy requests include the Accept: application/json; version=5 header. This pins Cornerstone to a known API shape and prevents breaking changes from upstream version bumps.
Proxy endpoints are scoped under /api/paperless/ and map to a curated subset of the Paperless-ngx API:
-
GET /api/paperless/documents-- Search/browse documents -
GET /api/paperless/documents/:id-- Single document metadata -
GET /api/paperless/documents/:id/thumb-- Document thumbnail (binary passthrough) -
GET /api/paperless/documents/:id/preview-- Document preview/PDF (binary passthrough) -
GET /api/paperless/tags-- List all Paperless-ngx tags
These proxy endpoints transform Paperless-ngx responses into Cornerstone's own response shapes, decoupling the client from the upstream API format. Only the fields needed by the Cornerstone UI are included.
- Security: The Paperless-ngx token never reaches the browser
-
Consistency: All API calls go through
/api/*, maintaining a single origin for the SPA -
Schema simplicity: One
document_linkstable instead of three junction tables -
Future entity types: Adding document links to new entities (e.g., subsidy programs, budget sources) requires only a new
entity_typeenum value and application-layer handlers - Configuration: Follows established env var patterns; admins familiar with OIDC config will recognize the pattern
- No referential integrity on entity_id: Application layer must handle orphan cleanup when entities are deleted
- Proxy maintenance: If Paperless-ngx changes their API significantly, the proxy layer needs updating (mitigated by version pinning)
- No offline access: Without caching, Paperless-ngx must be reachable for any document-related feature to work
- Binary passthrough for thumbnails/previews: The server must buffer and forward binary responses, adding memory pressure for large PDFs (mitigated by the small user count)