Choose Content Storage
Short answer: Eneo works without S3-compatible storage. New File and Icon content uses bounded PostgreSQL storage by default. Add a private compatible endpoint only when your capacity or availability needs justify operating it.
Start here
| I want to… | Go to |
|---|---|
| Run the simplest supported deployment | PostgreSQL only |
| Use the bundled private reference store | Bundled SeaweedFS |
| Connect MinIO or another compatible service | External endpoint |
| Understand ownership and RAG | How content flows |
| Download a processed file or recover its original | Download or recover |
| Plan backup, recovery, and failure handling | Operate it safely |
| Review lifecycle and integrity implementation | Architecture |
How content flows
File and Icon own the product meaning: filename, authorization, and where the content is used. The shared object-content module owns durable bytes, SHA-256, exact size and media type, placement, lifecycle, and physical deletion.
Each content item uses one byte authority. Eneo never dual-writes or silently falls back between PostgreSQL, object storage, or a filesystem.
Object storage and RAG solve different problems:
- durable content preserves the exact upload and named variants such as extracted text, transcription, model input, or preview;
- knowledge ingestion turns a chosen variant into searchable facts and vectors in PostgreSQL/pgvector;
- vectors do not replace the source file, and the source file does not replace the vector index.
What is adopted now?
| Product content | Current behavior |
|---|---|
| New File and Icon bytes | Use the shared lifecycle and PostgreSQL-inline byte backend |
| Existing File and Icon bytes | Copied, SHA-256 verified, switched to typed references, then removed from legacy columns during upgrade |
| InfoBlob knowledge generations and Flow artifacts | Remain separate follow-up work |
| Admin-selected placement and inline ↔ object-store migration | Remain a separate, verified migration workflow |
Enabling an endpoint makes the optional byte plane available; it does not move existing content or change producer placement by itself.
Download or recover a File
Eneo keeps processing views and original recovery explicit:
| Use | What Eneo returns |
|---|---|
| Normal download | The representation Eneo uses for the File, such as extracted text or an image prepared for model input |
| Original download | The exact persisted upload, with its original filename, media type, and byte length |
Both use signed access. Original-recovery links are deliberately short-lived:
callers can request between one second and one hour. Original recovery is
strict: Eneo never substitutes a processed representation when the original is
absent. Instead it returns 404 file_original_not_found (Eneo error 9045).
For a download button, keep the File ID and mint the URL when the user clicks:
const { url, expires_at } = await eneo.files.generateOriginalSignedUrl({
fileId,
contentDisposition: "attachment",
});
window.location.assign(url);Do not persist the returned URL or send it to analytics. It is a temporary
bearer credential; use expires_at to decide when the application must request
a new one. Eneo records who created an original-recovery link, its expiry, and
whether it was intended for download or inline display; the bearer token itself
is never written to the audit log.
The original response includes Repr-Digest for the SHA-256 of the complete
original. That value stays the same on an audio 206 range response because it
describes the full representation, not only the returned range. Range requests
remain audio-only; other file types use a complete download.
This contract is independent of placement. It behaves the same whether the bytes are stored inline in PostgreSQL or in an optional configured S3-compatible endpoint. Callers do not receive bucket, object-key, or provider details, and Eneo does not require S3-compatible storage.
When a view genuinely needs several attachment payloads, Eneo validates their database records as a bounded batch and then verifies each byte stream. Remote objects still require one content read each, but they do not add one database transaction per attachment.
Views that only show names, icons, publication state, and permissions do not retrieve attachment bodies. For example, the Space applications list reads metadata without opening PostgreSQL-inline or object-store bytes. This reduces storage work without changing the response or requiring object storage.
What happens when a File is deleted?
Eneo first checks the File and its generated derivatives against concrete product relations. A File cannot be deleted while it is attached to a chat, Assistant, App, or App run. The preview returns a short count for each blocking use; it never exposes storage keys or endpoint details.
Preview is advisory. Eneo repeats the same check while holding database locks before deletion, so a concurrent attachment cannot be silently removed. Removing a File from a chat or Assistant only removes that use; the reusable File remains until its owner deletes it. Organization or user offboarding keeps its existing database-cascade behavior.
Choose the right deployment
| Choice | Start here when… | Additional responsibility |
|---|---|---|
| PostgreSQL inline (default) | You want the fewest moving parts | Monitor database growth, WAL, and backup time |
| Bundled SeaweedFS | A single-node private reference store is enough | Operate its volume and pair its backup with PostgreSQL |
| External compatible endpoint | You already operate storage or need multi-node availability | Own endpoint security, TLS, capacity, credentials, and recovery |
Start inline unless measured volume, backup duration, database growth, or availability requirements say otherwise. “Enterprise” does not automatically mean “more services.”
Set up your choice
PostgreSQL only
Leave remote-only OBJECT_CONTENT_* settings absent and run Eneo normally:
docker compose up -d
curl -fsS https://eneo.example.eu/api/readyz \
| jq -e '.detail.object_content.code == "object_store_not_configured"'object_store_not_configured is healthy in this mode. PostgreSQL contains both
the control records and bounded payload bytes, so the normal PostgreSQL backup
is the complete content backup.
Set OBJECT_CONTENT_INLINE_MAXIMUM_BYTES in env_backend.env to a measured
deployment ceiling. It is an operational safety bound, not a hidden product
upload policy. It must be at least the largest configured File, image, or audio
upload limit. Eneo rejects a conflicting configuration at startup instead of
accepting an upload that PostgreSQL-inline storage cannot persist.
Operate it safely
Backup rule
- Only inline rows: PostgreSQL backup contains control and bytes.
- Any object-store rows: PostgreSQL and the endpoint are one recovery unit.
For the second case, stop writers and reconciliation, give both backups one
recovery ID, preserve OBJECT_CONTENT_DEPLOYMENT_ID and the bucket marker,
restore both halves in isolation, verify checksums and sample reads, then reopen
traffic. Never combine a newer database with an older bucket or the reverse.
During the File/Icon normalization upgrade, keep backend and worker producers stopped until Alembic succeeds. Retry a stopped migration before accepting new uploads; intervening writes deliberately block an unsafe retry.
The copy phase is row- and byte-bounded and hashes copied content before the final write fence. The fenced pass compares copied payloads with the legacy File/Icon columns once before dropping them. Measure this on a restored production-size database and plan a maintenance window proportional to total File/Icon bytes.
Failure rule
| Situation | Eneo behavior |
|---|---|
| No endpoint configured and no remote rows | Healthy inline operation |
| Endpoint temporarily unavailable | Inline work continues; remote operations return a typed temporary failure |
| Remote rows exist but configuration is missing | Fails closed with configuration_required |
| Bucket does not match this PostgreSQL database | Fails closed; Eneo does not adopt or mutate it |
Monitor PostgreSQL growth, endpoint capacity and latency, failed/pending content, delete backlog, reconciliation lag, temporary spool space, and stale multipart uploads. Keep endpoint APIs and consoles private.
FAQ
Do we need S3-compatible storage to run Eneo?
No. PostgreSQL inline is the default and needs no endpoint, bucket, credentials, or extra container.
Are files stored in both PostgreSQL and object storage?
No. PostgreSQL always holds identity and lifecycle facts. The bytes for each content item live either inline or in the configured endpoint.
Will enabling the endpoint move existing files?
No. Placement changes require an explicit copy, full verification, authority switch, and cleanup workflow.
Why can I not delete an uploaded File?
The File is still used by a chat, Assistant, App, or App run. Check its deletion preview, remove those uses deliberately, and try again. Eneo blocks the delete instead of silently breaking those resources.
What happens when an endpoint is down?
Content already owned by that endpoint is temporarily unavailable. Eneo does not create a second copy elsewhere as a fallback.
Can we use a European or self-hosted service?
Yes. Data resides wherever you operate the configured endpoint and its backups.
The region value is a signing scope, not a hosting decision.
Does this replace pgvector or document processing?
No. Object content preserves bytes; document processing produces searchable text and vectors. Both remain necessary for RAG.
Can we turn object storage off later?
Only after every remote item has been copied back, verified, and switched by a supported migration. Removing configuration while remote rows exist fails closed.