## Status
Draft / Request for Comment
## Related context
This proposal starte…d from a smaller performance issue: image previews in document tables should not load the original full-size image. That issue can be solved narrowly with image thumbnails, but the underlying concept is larger.
Invoice Ninja already connects clients, products, quotes, invoices, payments, documents, the client portal, React UI, Flutter clients, and integrations. A broader resource layer could turn documents, images, external links, generated files, and digital goods into a consistent client-facing delivery system.
This document is intentionally written as a master proposal. It should be split into smaller issues and work items before implementation.
---
# 1. Summary
Invoice Ninja currently supports generic document attachments. These documents can be attached to entities such as invoices, clients, products, projects, quotes, payments, expenses, and other business records.
The immediate problem is that image previews can be expensive when the UI uses full-size uploaded images. A practical first step is to introduce server-known thumbnails and previews for image documents.
The broader opportunity is to introduce a resource layer that can support:
* uploaded files;
* image derivatives;
* generated files;
* external links;
* digital goods;
* product-bound resources;
* resources attached to invoices, clients, projects, quotes, payments, and products;
* resources that become available after payment;
* public access until claimed;
* persistent authenticated portal access after claim;
* repeated downloads for entitled clients;
* optional tags/categories;
* global and product-specific resource automation;
* webhooks for external provisioning or fulfillment.
The goal is not to replace the existing document model in one step. The goal is to build a backward-compatible layer that starts with document image derivatives and can grow into a complete resource and delivery system.
---
# 2. Problem Statement
## 2.1 Immediate problem: image previews load full-size files
Image documents used in lists or tables can be several megabytes each. Loading original files for inline previews causes:
* heavy network usage;
* slow table rendering;
* high browser memory usage;
* poor UX on slow connections;
* aspect-ratio and layout issues;
* repeated loading across sessions or devices.
A client-side thumbnail cache helps only locally. It does not help other users, other devices, the Flutter apps, the client portal, WordPress integration, or API consumers.
## 2.2 Larger problem: documents are used for more than documents
Invoice Ninja documents are generic attachments. That is good and should remain true.
However, businesses often need more than file attachments:
* deliver paid digital files;
* provide manuals after purchase;
* attach project delivery packages;
* expose client-specific downloads;
* share external resources;
* create delivery links;
* revoke access after refunds;
* log access;
* trigger external systems after payment;
* let clients claim resources once and later access them through the portal.
These are not just “documents”. They are client-facing resources with access rules.
## 2.3 Current workaround patterns
Without a dedicated resource/delivery model, users may need to:
* send files manually by email;
* use external cloud folders;
* paste links into invoice notes;
* use custom fields as flags;
* poll the API from external automation tools;
* manually trigger fulfillment after payments;
* rely on public links that cannot be cleanly claimed, revoked, audited, or reused securely.
---
# 3. Goals
This proposal should enable the following outcomes.
## 3.1 Performance and preview goals
* Do not load original full-size images in document tables when a thumbnail exists.
* Generate and store reusable thumbnails/previews.
* Allow the server to generate, validate, normalize, convert, or regenerate derivatives.
* Keep original files unchanged.
* Keep non-image documents unaffected.
* Provide consistent derivative metadata to React, Flutter, admin portal, client portal, and API consumers.
## 3.2 Resource and delivery goals
* Represent uploaded files, images, generated files, external links, and digital goods as resources.
* Attach resources to invoices, clients, products, projects, quotes, payments, expenses, and other entities.
* Support product-bound resources.
* Support invoice/payment-gated access.
* Support public/invitation links that work until claimed.
* After claim, move access into the authenticated client portal.
* Allow repeated authenticated downloads while the entitlement remains valid.
* Provide visibility states such as internal-only, client-visible, gated, locked, available, revoked.
* Log resource access, downloads, claims, denies, and revocations.
## 3.3 Automation goals
* Support resource event webhooks globally.
* Support product-level or product-resource binding webhooks.
* Allow external systems to react when resources become available.
* Support use cases such as license creation, LMS access, fulfillment, external folder creation, CRM/ERP sync, and customer onboarding.
* Make webhook delivery asynchronous, retryable, signed, logged, and idempotent.
---
# 4. Non-Goals
The first implementation should not:
* replace the existing `documents` table;
* make all documents behave like images;
* force a full DAM/DMS implementation in one PR;
* expose private storage paths or raw private URLs;
* put entitlement logic into the frontend;
* trigger external webhooks on every download by default;
* require WordPress to become the access authority;
* require all clients to implement upload-side thumbnail generation immediately;
* implement tags as a dependency of the initial thumbnail fix.
---
# 5. Terminology
## Document
The existing generic uploaded file attachment.
Examples:
```text
invoice.pdf
receipt.jpg
contract.docx
manual.pdf
product-photo.png
delivery.zip
```
A document remains the source file. It should not be redefined as an image-only model.
## Image Document
A document whose verified MIME type is a supported raster image.
Initial support should be conservative:
```text
image/jpeg
image/png
image/webp, optional
```
Avoid first-version support for:
```text
svg
tiff
psd
animated gif
bmp
unknown binary files
```
## Derivative
A generated representation of a document or resource.
Examples:
```text
thumb
preview
display
pdf_preview
converted
```
A derivative is not the source of truth. It can be regenerated.
## Resource
A business-facing asset.
Examples:
```text
uploaded document
image
external link
generated file
digital product file
delivery bundle
license file
manual
client-specific project folder
```
## Resource Relation
A link between a resource and a business entity.
Examples:
```text
resource belongs to product
resource attached to invoice
resource visible to client
resource related to project
resource delivered through quote acceptance
```
## Entitlement
A server-side rule that decides whether a user, client, contact, token, or portal session may access a resource.
Examples:
```text
internal_only
client_visible
invoice_paid
manual_release
product_purchased
until_claimed
```
## Access Grant
A concrete access permission or token.
Examples:
```text
public claim token
portal grant
signed temporary access
manual admin grant
```
## Claim
A transition from anonymous or invitation-based access to authenticated client/contact access.
Important behavior:
```text
Before claim:
public or invitation link can be used.
On claim:
user authenticates or verifies identity.
After claim:
anonymous token is no longer reusable.
access continues through the authenticated client portal.
```
---
# 6. Core Design Principles
## 6.1 Documents stay generic
Do not turn `Document` into an image model.
Correct framing:
```text
Documents are generic polymorphic attachments.
Supported image documents may have derivatives.
Resources provide a business-facing layer above documents.
```
## 6.2 Server is authoritative
Client-generated thumbnails should be treated as candidate derivatives only.
The server may:
```text
accept
reject
ignore
regenerate
normalize
convert
replace
```
The frontend must not assume that a client-generated thumbnail is trusted or final.
## 6.3 Access is always server-side
The frontend may display the state returned by the API, but it must not decide entitlement.
Bad:
```text
if invoice.status == paid:
show download
```
Good:
```text
Ask the server whether the resource is available.
Render the response.
```
## 6.4 Public access must be limited
Public links may be useful for first access, but should not become permanent public download URLs.
Preferred behavior:
```text
public claim link works until claimed
authenticated portal access persists after claim
public token is invalidated after claim
```
## 6.5 Digital goods should be redownloadable
For paid digital goods, the default should be:
```text
Once authenticated and entitled, the client/contact can download again later.
```
Optional restrictions can exist:
```text
expires_at
max_downloads
manual revocation
revoke_on_refund
revoke_on_chargeback
revoke_on_invoice_cancelled
```
But repeated authenticated access should be supported.
---
# 7. Proposed Architecture
## 7.1 Layered model
```text
Existing Layer
--------------
Document
Generic uploaded file attachment.
New Derivative Layer
--------------------
DocumentDerivative
Thumbnail / preview / display version of a document.
New Resource Layer
------------------
Resource
Business-facing asset.
ResourceRelation
Connects resource to invoice/product/client/project/etc.
ResourceEntitlement
Defines access policy.
ResourceAccessGrant
Concrete access grant or token.
ResourceAccessLog
Audit trail.
ResourceWebhookEndpoint
Configured external endpoint.
ResourceAutomationRule
Event-to-endpoint mapping.
ResourceWebhookDelivery
Delivery log, retry state, response state.
```
## 7.2 Conceptual flow
```mermaid
flowchart TD
A[Document upload] --> B{Supported image?}
B -- no --> C[Store document as today]
B -- yes --> D[Create derivative candidates]
D --> E[Server validates / regenerates]
E --> F[Store thumbnails/previews]
F --> G[API exposes derivatives]
G --> H[React / Flutter / Portal use thumbnail]
H --> I[Original loaded only on explicit open]
```
## 7.3 Digital delivery flow
```mermaid
flowchart TD
A[Product has resource binding] --> B[Client buys product / receives invoice]
B --> C[Invoice unpaid]
C --> D[Portal shows resource locked or pending]
D --> E[Invoice paid]
E --> F[Server marks entitlement available]
F --> G[Resource automation events fire]
F --> H[Client can claim/access resource]
H --> I[After claim: portal access persists]
I --> J[Client can redownload while entitlement is valid]
```
---
# 8. Data Model Proposal
This can be implemented incrementally.
## 8.1 Phase 1: Document derivatives only
### `document_derivatives`
```text
id
company_id
document_id
variant thumb | preview | display
status pending | ready | failed | missing
mime_type
width
height
size
storage_disk
storage_path
checksum
policy_version
metadata JSON nullable
created_at
updated_at
deleted_at nullable
```
Notes:
* `document_id` references the existing document.
* `variant` should be unique per document/policy version.
* `status` allows lazy generation or async queues.
* `policy_version` allows future regeneration.
* Deleting a document must delete its derivatives.
### Example derivative metadata
```json
{
"variant": "thumb",
"status": "ready",
"mime_type": "image/webp",
"width": 150,
"height": 113,
"size": 8120,
"policy_version": 1
}
```
## 8.2 Phase 2+: Resource layer
### `resources`
```text
id
company_id
type document | image | external_link | generated | bundle
source_type upload | document | external_url | generated | virtual
document_id nullable
title
description
mime_type
size
external_url nullable, encrypted if needed
metadata JSON nullable
visibility_default internal | client_visible | gated
status active | archived | revoked
created_by_user_id
created_at
updated_at
deleted_at nullable
```
### `resource_relations`
```text
id
company_id
resource_id
related_type invoice | quote | product | project | client | payment | expense | vendor | task | recurring_invoice | recurring_quote | purchase_order
related_id
role attachment | preview_image | delivery | receipt | contract | product_file | manual | license | evidence | internal_note
sort_order
created_at
updated_at
deleted_at nullable
```
### `resource_derivatives`
```text
id
company_id
resource_id
document_id nullable
variant thumb | preview | display | pdf_preview | converted
status pending | ready | failed | missing
mime_type
width
height
size
storage_disk
storage_path
checksum
policy_version
metadata JSON nullable
created_at
updated_at
deleted_at nullable
```
### `resource_entitlements`
```text
id
company_id
resource_id
policy_type
policy_config JSON
enabled
created_at
updated_at
deleted_at nullable
```
Possible `policy_type` values:
```text
internal_only
client_visible
manual_release
invoice_sent
invoice_paid
invoice_paid_in_full
invoice_partially_paid
quote_accepted
payment_received
product_purchased
subscription_active
until_claimed
```
Example `policy_config`:
```json
{
"invoice_id": "hashed_invoice_id",
"minimum_paid_percent": 100,
"allow_manual_override": true,
"revoke_on_refund": false,
"revoke_on_invoice_cancelled": true,
"revoke_on_reversal": true
}
```
### `resource_access_grants`
```text
id
company_id
resource_id
client_id nullable
contact_id nullable
invoice_id nullable
payment_id nullable
token_hash nullable
grant_type portal | public_token | signed_redirect | api | manual
claim_mode none | until_claimed | claim_once_then_persist
max_claims nullable
claimed_at nullable
claimed_by_contact_id nullable
expires_at nullable
revoked_at nullable
created_at
updated_at
```
### `resource_access_logs`
```text
id
company_id
resource_id
grant_id nullable
client_id nullable
contact_id nullable
user_id nullable
event_type viewed | downloaded | claimed | denied | revoked | expired
ip_address_hash nullable
user_agent_hash nullable
metadata JSON nullable
created_at
```
### Optional future: `resource_tags`
Tags should not block the initial implementation. If added later, possible values include:
```text
contract
receipt
tax
manual
license
delivery
product_file
before
after
logo
internal
client_visible
warranty
onboarding
```
---
# 9. Derivative Pipeline
## 9.1 Upload behavior
Existing document uploads should continue to work:
```text
documents[0] = original.jpg
documents[1] = contract.pdf
```
New clients may optionally submit derivative candidates:
```text
documents[0] = original.jpg
document_derivatives[0][thumb] = original_thumb_150.webp
document_derivatives[0][preview] = original_preview_400.webp
```
Rules:
```text
index must match documents[index]
derivatives are only accepted for supported image documents
server validates actual MIME type and dimensions
server may ignore or regenerate candidate derivatives
non-image files ignore/reject derivative candidates
```
## 9.2 Supported derivatives
Initial variants:
```text
thumb
max edge: 150px
usage: tables, compact lists
preview
max edge: 400px or 800px
usage: detail preview, modal, portal preview
display
max edge: 1200px
usage: optional optimized display version
```
## 9.3 Server-side generation
The server should be able to generate derivatives in three ways:
```text
on upload
generate immediately or queue job
lazy
generate on first derivative request
backfill
generate for existing documents via command/job
```
Suggested command:
```text
php artisan ninja:document-derivatives:generate
```
Possible options:
```text
--company=
--document=
--missing-only
--force
--variant=thumb
--policy-version=1
```
## 9.4 Validation rules
Required checks:
```text
original must be supported image MIME type
thumbnail must be supported image MIME type
thumbnail dimensions must be within allowed range
thumbnail file size must be below allowed limit
never trust client-provided path
never expose derivative without authorization
strip metadata where appropriate
reject SVG as thumbnail format
```
Recommended initial thumbnail output:
```text
image/webp where supported
image/jpeg fallback
```
---
# 10. API Proposal
## 10.1 Existing document API remains backward-compatible
Existing routes and payloads should continue to work.
Examples:
```text
GET /api/v1/documents
GET /api/v1/documents/{document}
GET /api/v1/documents/{document}/download
PUT /api/v1/invoices/{invoice}/upload
PUT /api/v1/products/{product}/upload
PUT /api/v1/projects/{project}/upload
PUT /api/v1/quotes/{quote}/upload
```
## 10.2 New document derivative endpoints
```text
GET /api/v1/documents/{document}/derivatives/{variant}
POST /api/v1/documents/{document}/derivatives/regenerate
```
Alternative if the project prefers `thumbnail` naming:
```text
GET /api/v1/documents/{document}/thumbnail?variant=thumb
```
Preferred:
```text
GET /api/v1/documents/{document}/derivatives/thumb
```
Reason: this leaves room for `preview`, `display`, and future derivative types.
## 10.3 Document response extension
Add a backward-compatible `derivatives` object.
```json
{
"id": "hashed_document_id",
"name": "receipt.jpg",
"type": "jpg",
"url": "/api/v1/documents/hashed_document_id/download",
"preview": "",
"width": 3024,
"height": 4032,
"size": 3456789,
"is_public": false,
"derivatives": {
"thumb": {
"status": "ready",
"url": "/api/v1/documents/hashed_document_id/derivatives/thumb",
"width": 150,
"height": 113,
"mime_type": "image/webp",
"size": 8120
},
"preview": {
"status": "ready",
"url": "/api/v1/documents/hashed_document_id/derivatives/preview",
"width": 400,
"height": 300,
"mime_type": "image/webp",
"size": 32110
}
}
}
```
Do not overload the existing `preview` field with incompatible JSON if existing clients expect a string.
## 10.4 Resource API
Possible endpoints:
```text
GET /api/v1/resources
POST /api/v1/resources
GET /api/v1/resources/{resource}
PUT /api/v1/resources/{resource}
DELETE /api/v1/resources/{resource}
POST /api/v1/resources/{resource}/relations
DELETE /api/v1/resources/{resource}/relations/{relation}
POST /api/v1/resources/{resource}/entitlements
PUT /api/v1/resources/{resource}/entitlements/{entitlement}
DELETE /api/v1/resources/{resource}/entitlements/{entitlement}
POST /api/v1/resources/{resource}/access
POST /api/v1/resource_grants/{grant}/claim
GET /api/v1/client_portal/resources
GET /api/v1/client_portal/resources/{resource}
```
## 10.5 Resource response example
```json
{
"id": "resource_id",
"type": "document",
"title": "Digital delivery package",
"status": "active",
"relations": [
{
"related_type": "invoice",
"related_id": "invoice_id",
"role": "delivery"
},
{
"related_type": "product",
"related_id": "product_id",
"role": "product_file"
}
],
"access": {
"state": "available",
"mode": "portal",
"claim_mode": "claim_once_then_persist",
"redownload": "authenticated_persistent"
},
"derivatives": {
"thumb": {
"status": "ready",
"url": "/api/v1/resources/resource_id/derivatives/thumb"
}
}
}
```
## 10.6 Access decision endpoint
```text
POST /api/v1/resources/{resource}/access
```
Possible responses:
```text
200 available
202 pending
402 payment_required
403 forbidden
404 not_visible
410 revoked
423 locked
```
Example:
```json
{
"state": "payment_required",
"message": "This resource becomes available after the invoice is paid.",
"invoice_id": "invoice_id",
"portal_url": "/client/invoices/invoice_id"
}
```
---
# 11. Access Model
## 11.1 Internal access
Company users may manage resources according to normal permissions.
Internal access should support:
```text
view
create
edit
delete
attach
detach
release
revoke
regenerate_derivatives
retry_webhooks
view_access_logs
```
## 11.2 Client portal access
Client contacts should only see resources that are:
```text
related to their client/contact/invoice/project/product
allowed by entitlement policy
not revoked
not expired
not hidden by visibility rules
```
Client portal should support:
```text
available resources
locked resources, where useful
resources available after payment
claimed resources
download history, optional
```
## 11.3 Public link access
Public link access should be explicit and limited.
Use cases:
```text
claim link from invoice email
quote acceptance link
payment success link
one-time onboarding link
```
Public links should not expose raw file storage URLs.
## 11.4 Until claimed
`until_claimed` should be a first-class access mode.
Behavior:
```text
Before claim:
public token works.
During claim:
user authenticates, verifies email, or logs into portal.
On successful claim:
grant is bound to client/contact.
token is marked claimed.
anonymous reuse is blocked.
portal entitlement becomes active.
After claim:
client/contact can redownload from portal.
public token no longer acts as reusable download link.
```
## 11.5 Repeated authenticated downloads
Digital resources should support repeated downloads by default after entitlement is granted.
Example:
```text
A client buys a digital product.
The invoice is paid.
The client claims the resource.
The client can log in again next month and download it again.
```
Optional restrictions:
```text
max_downloads
expires_at
manual revoke
revoke on refund
revoke on chargeback
revoke on invoice cancellation
```
---
# 12. Payment-Gated Delivery
## 12.1 Common states
```text
internal_only
locked
visible_pending_payment
available_after_payment
available
claimed
revoked
expired
```
## 12.2 Invoice-based entitlement
Example:
```json
{
"policy_type": "invoice_paid_in_full",
"policy_config": {
"invoice_id": "hashed_invoice_id",
"minimum_paid_percent": 100,
"allow_manual_override": true,
"revoke_on_refund": false,
"revoke_on_invoice_cancelled": true,
"revoke_on_reversal": true
}
}
```
## 12.3 Edge cases
The access service should consider:
```text
partially paid invoices
overpayments
refunds
chargebacks
cancelled invoices
reversed invoices
manual release
manual revoke
changed invoice line items
deleted documents
deleted clients
merged clients
multiple contacts on same client
```
## 12.4 Payment success experience
After payment:
```text
payment success page can show newly available resources
invoice page can show downloads
client portal can show "My Resources"
resource automation events can be queued
```
---
# 13. External Resources
External resources should be first-class resources, not fake documents.
Examples:
```text
private project folder
Google Drive link
S3 object
Dropbox file
external course platform
software license portal
documentation page
onboarding form
```
## 13.1 Modes
### Reference mode
Invoice Ninja stores the link and displays it only after authorization.
Pros:
```text
simple
provider-agnostic
```
Cons:
```text
link may be forwarded
external permissions may not match Invoice Ninja
limited audit control
```
### Signed redirect mode
Invoice Ninja checks access and then redirects to a short-lived URL or provider-signed link.
Preferred for paid/gated resources.
### Proxy mode
Invoice Ninja streams the external resource after checking access.
This should be optional and carefully restricted because arbitrary URL fetching can create security and performance risks.
## 13.2 Security rules for external resources
```text
do not proxy arbitrary URLs by default
validate allowed URL schemes
reject local/private network targets
avoid leaking secret URLs
store provider credentials securely
log access
prefer signed redirects where available
```
---
# 14. Resource Automation and Webhooks
## 14.1 Problem
Some digital delivery workflows require external actions when a resource becomes available.
Examples:
```text
create a software license
provision LMS/course access
create a SaaS account
create/share a client folder
trigger fulfillment
sync delivery state to CRM/ERP
generate a private download package
notify an external system
```
## 14.2 Design principle
Do not attach a raw `webhook_url` directly to every product and call it synchronously during checkout or download.
Instead, implement event-based resource automation.
## 14.3 Automation scopes
Support three levels:
```text
Global company rules
Default automation for all resource events.
Product / product-resource binding rules
Automation for a specific product or resource sold through that product.
Resource / entitlement rules
Specific automation for one concrete resource or delivery rule.
```
Inheritance:
```text
company defaults
-> product binding
-> resource relation
-> entitlement-specific behavior
```
## 14.4 Suggested tables
### `resource_webhook_endpoints`
```text
id
company_id
name
target_url
method POST | PUT
headers JSON encrypted/secured
secret encrypted
enabled
timeout_seconds
created_at
updated_at
deleted_at nullable
```
### `resource_automation_rules`
```text
id
company_id
scope_type company | product | resource | entitlement
scope_id nullable
event
endpoint_id
enabled
payload_template
retry_policy
created_at
updated_at
deleted_at nullable
```
### `resource_webhook_deliveries`
```text
id
company_id
event_id
event
endpoint_id
resource_id nullable
invoice_id nullable
product_id nullable
client_id nullable
contact_id nullable
status pending | delivered | failed | retrying | disabled
attempts
next_attempt_at nullable
last_status_code nullable
last_response_excerpt nullable
idempotency_key
created_at
updated_at
```
## 14.5 Event types
Recommended event names:
```text
resource.created
resource.updated
resource.attached
resource.detached
resource.derivative.ready
resource.derivative.failed
resource.entitlement.created
resource.entitlement.available
resource.entitlement.revoked
resource.entitlement.expired
resource.grant.created
resource.claimed
resource.access.granted
resource.access.denied
resource.downloaded
resource.delivery.pending
resource.delivery.completed
resource.delivery.failed
invoice.paid.resources_available
product.purchased.resource_pending
```
The most important delivery event is usually:
```text
resource.entitlement.available
```
not:
```text
resource.downloaded
```
Downloads can be logged or optionally webhooked, but should not trigger external provisioning by default.
## 14.6 Payload example
```json
{
"event": "resource.entitlement.available",
"event_id": "evt_123",
"idempotency_key": "evt_123",
"company": {
"id": "company_id"
},
"client": {
"id": "client_id",
"email": "[email protected]"
},
"contact": {
"id": "contact_id",
"email": "[email protected]"
},
"invoice": {
"id": "invoice_id",
"number": "INV-001",
"status": "paid"
},
"product": {
"id": "product_id",
"product_key": "digital-course"
},
"resource": {
"id": "resource_id",
"type": "digital_good",
"title": "Course Access",
"access_state": "available"
},
"access": {
"claim_mode": "claim_once_then_persist",
"portal_url": "portal_resource_url",
"download_url": null
}
}
```
Do not include permanent private download URLs by default.
## 14.7 Delivery requirements
Webhook delivery should be:
```text
asynchronous
queued
retryable
signed
logged
idempotent
manually retryable
timeout-limited
safe if endpoint is down
```
A failed webhook must not break invoice payment or portal rendering. It should create an admin-visible delivery failure that can be retried.
## 14.8 Required headers
Suggested headers:
```text
X-InvoiceNinja-Event
X-InvoiceNinja-Event-Id
X-InvoiceNinja-Delivery-Id
X-InvoiceNinja-Signature
X-InvoiceNinja-Timestamp
X-Idempotency-Key
```
---
# 15. React UI Implications
The React UI should not solve this only in `DocumentsTable`.
## 15.1 Shared components
Introduce shared components:
```text
ResourceThumbnail
ResourcePreview
ResourceAccessBadge
ResourceRelationList
ResourcePicker
ResourceUploadDropzone
ResourceDeliveryPanel
ResourceAutomationPanel
EntityResourcesTab
```
## 15.2 Affected areas
```text
DocumentsTable
entity document tabs
client documents
invoice documents
quote documents
project documents
product documents
expense receipts
payment attachments
purchase order attachments
recurring entity documents
bulk document views
product edit/detail pages
resource delivery settings
settings / global resource automation
```
## 15.3 Rendering priority
For images:
```text
1. derivative thumb
2. derivative preview
3. existing preview field
4. generic file placeholder
5. original file only on explicit open
```
Do not embed the original image in tables if a derivative exists.
## 15.4 Resource delivery UI
Admin-facing UI should support:
```text
attach resource to invoice/product/project/client
set access policy
mark internal-only or client-visible
set available after payment
set manual release
create claim link
view access logs
retry failed automation
regenerate derivatives
```
---
# 16. Flutter and Admin Portal Implications
There are two Flutter contexts:
```text
admin-portal
Existing Redux-based Flutter app.
flutter
Newer rebuild with local persistence, page-by-page loading, Drift, and offline mutation outbox.
```
Both need to be considered.
## 16.1 Minimal support
Both clients should eventually:
```text
parse derivative metadata
render thumbnail where available
fall back safely when missing
keep original open/download behavior unchanged
display resource availability state
avoid loading originals in lists
```
## 16.2 New Flutter rebuild
Because the new app uses local persistence and an outbox, resource models should be designed for offline-safe metadata sync.
Needed models/tables:
```text
Resource
ResourceRelation
ResourceDerivative
ResourceEntitlementSummary
ResourceAccessState
ResourceWebhookSummary, optional admin-only
```
The UI should read from local persistence and let repositories/services sync from the API.
## 16.3 Offline behavior
Offline clients should:
```text
show cached resource metadata
show cached thumbnails if available
not assume access if entitlement state is stale
queue uploads where supported
sync derivative/resource metadata later
```
For gated resources, online server confirmation should be required before first download.
---
# 17. Client Portal Implications
The client portal is the natural home for authenticated resource delivery.
## 17.1 Portal surfaces
Possible surfaces:
```text
Invoice page:
resources available after payment
locked resources
download resources after payment
Payment success page:
newly available resources
Client dashboard:
My Resources
Project Files
Product Downloads
Project page:
project delivery resources
Product purchase page:
product files/resources
```
## 17.2 Portal rules
```text
never expose raw private storage URLs
ask backend for access
show locked state only when useful
hide sensitive metadata until allowed
support repeated authenticated downloads
support claim flow
support revoked/expired states
```
## 17.3 Claim flow in portal
```text
client opens claim link
client logs in or verifies email
server validates token
server binds access to contact/client
server invalidates anonymous token
portal shows resource
client can redownload later
```
---
# 18. WordPress Plugin Implications
WordPress should not be the authority for private resource access. Invoice Ninja should remain the entitlement and access server.
## 18.1 Useful integration points
WordPress may:
```text
display public product media
display public resource metadata
show that paid resources exist
render purchase buttons
sync public product/resource data
send users to Invoice Ninja portal
use SSO handoff where safe
```
WordPress should not:
```text
store permanent private file URLs
decide whether a paid resource is available
bypass Invoice Ninja access checks
sync secret external URLs into public pages
```
## 18.2 Product resources
If products have resources attached, WordPress could display:
```text
product images
public previews
manual/documentation teaser
resource availability note
```
For gated delivery:
```text
User purchases product
Invoice Ninja handles entitlement
WordPress redirects/hands off to portal
Client downloads from portal
```
## 18.3 Shortcode possibilities
Future shortcodes might include:
```text
[invoiceninja_resource_preview product_id="..."]
[invoiceninja_resource_status product_id="..."]
[invoiceninja_client_resources]
```
But gated downloads should point back to Invoice Ninja portal or access endpoints.
---
# 19. Templates and Emails
Templates should support resource-aware messaging without exposing private URLs.
Possible variables:
```text
$has_delivery_resources
$delivery_resources_count
$resources_available_after_payment
$client_portal_resources_url
$resource_claim_url
$resource_access_state
```
Example email copy:
```text
Your files will become available in the client portal after payment.
```
```text
Your digital goods are now available:
[Open Client Portal]
```
```text
Please claim your resources:
[Claim Resources]
```
Rules:
```text
email should link to claim/portal URL
email should not include permanent private file URL
locked resources should not expose sensitive filenames unless allowed
```
---
# 20. Security and Privacy
## 20.1 Authorization parity
Derivative access must use the same authorization model as the original document or resource.
Do not serve private thumbnails from public storage paths.
## 20.2 Token safety
```text
store token hashes, not raw tokens
support expiration
support revocation
support claim-once behavior
log claim attempts
rate-limit claim attempts
```
## 20.3 External URLs
```text
do not blindly proxy arbitrary URLs
block local/private network targets if proxying
validate URL schemes
store secrets encrypted
do not leak external secret URLs to unauthorized users
```
## 20.4 Metadata exposure
Locked resources may show:
```text
1 file available after payment
```
But should not necessarily show:
```text
filename
thumbnail
external URL
private description
```
unless the policy allows it.
## 20.5 Audit
Log:
```text
resource viewed
resource downloaded
claim succeeded
claim failed
access denied
entitlement revoked
webhook delivered
webhook failed
manual retry
```
---
# 21. Migration Strategy
## 21.1 Phase 1: document derivatives
Add `document_derivatives`.
No breaking changes.
Existing documents remain valid.
## 21.2 Phase 2: derivative API
Expose derivatives in document responses.
Add derivative endpoints.
Existing clients ignore unknown fields.
## 21.3 Phase 3: React preview usage
Update `DocumentsTable` and shared preview components.
Use derivatives when present.
Fallback to current behavior when missing.
## 21.4 Phase 4: Flutter/admin metadata
Add read-side support for derivative metadata.
No upload changes required initially.
## 21.5 Phase 5: resource layer
Introduce resources and relations.
Map existing documents to resources lazily or when needed.
## 21.6 Phase 6: portal delivery
Add resource listing and access state in client portal.
Support claim and authenticated redownload.
## 21.7 Phase 7: automation
Add resource webhooks and event delivery.
Start with global rules and product bindings.
## 21.8 Phase 8: WordPress handoff
Expose public metadata and portal handoff.
Avoid private URL sync.
## 21.9 Phase 9: tags
Add resource tags/categories.
---
# 22. Proposed Work Items
## Work Item 1: Backend document derivatives data model
Repository:
```text
invoiceninja/invoiceninja
```
Tasks:
```text
add document_derivatives table
add DocumentDerivative model
add relation from Document to derivatives
add derivative repository/service
delete derivatives when document is deleted
add tests
```
Acceptance criteria:
```text
documents can have multiple derivatives
non-image documents remain unaffected
document deletion removes derivative files/records
existing document API remains compatible
```
## Work Item 2: Backend derivative generation service
Repository:
```text
invoiceninja/invoiceninja
```
Tasks:
```text
validate supported image MIME types
generate thumb/preview variants
support policy_version
strip metadata where appropriate
store derivatives on configured disk
queue or lazily generate missing derivatives
add regenerate command
add tests
```
Acceptance criteria:
```text
jpg/png images get derivatives
unsupported files do not break
server can regenerate derivatives
failed generation is tracked
```
## Work Item 3: Upload-side derivative candidates
Repository:
```text
invoiceninja/invoiceninja
```
Tasks:
```text
allow optional document_derivatives[index][variant]
validate candidates
store or regenerate candidates
ignore/reject invalid candidates
do not trust client-provided metadata
add tests
```
Acceptance criteria:
```text
existing uploads continue to work
new clients can submit thumbnails
server remains authoritative
invalid thumbnails cannot bypass validation
```
## Work Item 4: Document API response and derivative endpoints
Repository:
```text
invoiceninja/invoiceninja
```
Tasks:
```text
extend DocumentTransformer with derivatives
add GET document derivative endpoint
add POST regenerate endpoint, admin-only
ensure authorization parity
add tests for unauthorized access
```
Acceptance criteria:
```text
document response includes derivatives where available
old clients remain compatible
private derivatives are protected
```
## Work Item 5: React document preview improvements
Repository:
```text
invoiceninja/ui
```
Tasks:
```text
update DocumentsTable to prefer derivative thumb
add shared thumbnail/preview component
preserve aspect ratio
open original only on explicit click
fallback safely if derivative missing
optional client-side thumbnail generation during upload
```
Acceptance criteria:
```text
tables do not load full-size images when thumbs exist
aspect ratio is preserved
original opens on click
non-image documents show normal placeholder
```
## Work Item 6: Flutter admin portal support
Repository:
```text
invoiceninja/admin-portal
```
Tasks:
```text
parse derivative metadata
show thumbnails where available
fallback to current behavior
avoid breaking existing Redux/state structure
```
Acceptance criteria:
```text
existing documents still render
image documents can show thumbnail
missing derivatives do not break UI
```
## Work Item 7: New Flutter app resource/derivative models
Repository:
```text
invoiceninja/flutter
```
Tasks:
```text
add derivative DTO/model
add local persistence fields/tables
support repository sync
support cached thumbnail metadata
plan resource models for future layer
```
Acceptance criteria:
```text
derivative metadata syncs into local state
UI can render thumbnail from local data
offline behavior remains safe
```
## Work Item 8: Resource layer backend
Repository:
```text
invoiceninja/invoiceninja
```
Tasks:
```text
add resources table
add resource_relations table
add resource_derivatives or map document_derivatives
add resource API
map documents to resources where needed
add policies/permissions
add tests
```
Acceptance criteria:
```text
resources can represent documents and external links
resources can relate to invoices/products/clients/projects
existing documents remain unchanged
```
## Work Item 9: Resource entitlements and access grants
Repository:
```text
invoiceninja/invoiceninja
```
Tasks:
```text
add resource_entitlements
add resource_access_grants
add access decision service
support invoice-paid policies
support manual release/revoke
support until_claimed
support claim_once_then_persist
add tests
```
Acceptance criteria:
```text
resources can be locked until invoice is paid
public links can be claimed once
claimed resources become portal-accessible
authenticated clients can redownload
revoked resources are blocked
```
## Work Item 10: Client portal resource delivery
Repository:
```text
invoiceninja/invoiceninja
```
Tasks:
```text
add portal resource listing
add invoice page resource section
add payment success resource section
add claim flow
add locked/available/revoked UI states
add tests
```
Acceptance criteria:
```text
clients see available resources
clients see payment-gated resources where appropriate
claim links work until claimed
portal access persists after claim
```
## Work Item 11: Resource automation and webhooks
Repository:
```text
invoiceninja/invoiceninja
```
Tasks:
```text
add resource_webhook_endpoints
add resource_automation_rules
add resource_webhook_deliveries
add event dispatcher
add HMAC signatures
add retry/backoff
add delivery logs
add manual retry
add product binding support
add tests
```
Acceptance criteria:
```text
global rules can trigger on resource events
product-level rules can trigger on product resource availability
failed delivery retries
events are signed and idempotent
payment/portal flow does not fail if webhook target is down
```
## Work Item 12: React resource management UI
Repository:
```text
invoiceninja/ui
```
Tasks:
```text
resource list/picker
resource relation UI
product resource binding UI
invoice/project/client resource tabs
entitlement editor
manual release/revoke actions
webhook rule UI
delivery logs
```
Acceptance criteria:
```text
users can attach resources to business entities
users can configure access policy
users can configure automation globally/per product
users can inspect access and delivery state
```
## Work Item 13: WordPress public metadata and portal handoff
Repository:
```text
invoiceninja/wordpress
```
Tasks:
```text
sync public product resource metadata
show public previews
show availability notes
avoid syncing private URLs
support portal/SSO handoff for gated resources
add shortcodes if useful
```
Acceptance criteria:
```text
WordPress can show public resource metadata
gated downloads stay in Invoice Ninja
private resource URLs are not exposed
```
## Work Item 14: Tags/categories for resources
Repositories:
```text
invoiceninja/invoiceninja
invoiceninja/ui
invoiceninja/flutter
invoiceninja/admin-portal
```
Tasks:
```text
add tags/categories for resources
add filters
add API support
add UI support
add optional portal display
```
Acceptance criteria:
```text
resources can be categorized
users can filter resources
tags do not affect access unless explicitly configured
```
---
# 23. Acceptance Criteria for the Overall Proposal
## Derivatives
```text
existing uploads still work
non-image documents are unaffected
supported images get thumbnails/previews
server can generate and regenerate derivatives
React tables use thumbnails
original file loads only on explicit open
authorization matches original document
document deletion cleans up derivatives
```
## Resources
```text
resources can represent documents, images, external links, generated files, and bundles
resources can attach to invoices, products, clients, projects, quotes, payments, and expenses
resources can be internal-only, client-visible, gated, available, claimed, revoked
external resources do not leak private URLs
```
## Entitlements
```text
payment-gated resources unlock after valid payment
until_claimed links work for first access
claimed resources become persistent portal resources
authenticated clients can redownload
manual release/revoke works
refund/cancellation/reversal behavior is configurable
```
## Portal
```text
clients can see resources in relevant context
locked resources can be shown without leaking sensitive metadata
payment success page can show newly available resources
claim flow works
portal redownload works
```
## Automation
```text
global resource webhooks work
product-bound webhooks work
resource events are queued
events are signed
events are retryable
events are idempotent
failed deliveries are visible and retryable
```
## WordPress
```text
public resource metadata can sync
private resources are not exposed
gated access is handed off to Invoice Ninja
WordPress does not decide entitlement
```
---
# 24. Open Questions
## Product/resource binding
Should resources be attached directly to products, invoice line items, or both?
Possible options:
```text
product-level binding
line-item-level binding
invoice-level binding
all of the above
```
## Entitlement source
Should payment-gated access be based on:
```text
invoice paid in full
specific line item paid
minimum paid percentage
manual release
subscription active
```
## Revoke behavior
What should happen on:
```text
refund
partial refund
chargeback
invoice cancellation
invoice reversal
client merge
contact deletion
```
## Public metadata
How much information should a locked resource expose?
Options:
```text
show nothing
show count only
show generic title
show full title/description
show thumbnail
```
## Webhooks
Should resource automation reuse existing webhook infrastructure, or use a new delivery system with retry/idempotency from the start?
## Tags
Should tags extend the existing tag model, or should resources have their own categories/tags?
## WordPress
Should WordPress only show public metadata and portal links, or should it also support a gated resource widget that calls Invoice Ninja server-side?
---
# 25. Suggested Implementation Order
Recommended order:
```text
1. Backend document derivatives.
2. Document derivative API.
3. React DocumentsTable thumbnail usage.
4. Flutter/admin read-side derivative support.
5. Resource model and relations.
6. Entitlements and access grants.
7. Client portal delivery and claim flow.
8. Resource automation/webhooks.
9. Product binding UI.
10. WordPress metadata/portal handoff.
11. Tags/categories.
```
This keeps the initial fix small and useful while creating a clear path toward a larger digital delivery and resource management system.
---
# 26. Final Recommendation
Do not solve the preview issue as a one-off frontend-only image optimization.
The immediate fix should be:
```text
Document image derivatives:
thumbnails and previews for supported raster image documents.
```
The long-term architecture should be:
```text
Resources:
business-facing assets connected to Invoice Ninja entities.
Entitlements:
server-side rules for who can access what and when.
Access grants:
claim links, portal access, repeated authenticated downloads.
Automation:
global and product-bound resource events for external systems.
```
This approach preserves the existing document system, fixes the current image preview performance problem, and opens a path for digital goods, external resources, payment-gated delivery, client portal downloads, WordPress handoff, and external fulfillment automation.