Design Doc: Sentin8:Storage — Object Storage Service
1. Summary
Sentin8:Storage is the platform's object-storage service. Buckets are
per-environment ({vertical}-* naming with env-scoped RLS), objects
are written/read/copied/moved/deleted through an M2M-authenticated API,
and time-boxed signed URLs let untrusted clients fetch or upload
without holding platform credentials. Direct PostgreSQL or object-
storage access from domain services is prohibited — everything goes
through the shared Storage client.
2. Motivation
- One object plane. AI chunk text, trace archives, app uploads, and vertical blobs all share one storage plane with tenant-isolated bucket semantics — no vertical owns an S3 account or a filesystem.
- All access requires a token. Every read/write requires an Sentin8:Auth-issued token; signed URLs are the escape hatch for unauthenticated consumers (browser uploads/downloads) and are time-boxed and auditable.
- Path traversal is a security invariant. Keys must stay inside their bucket root — traversal attempts are rejected before any IO, not sanitized after.
- Idempotent, content-addressed friendly. Content-addressed
verticals (email blobs etc.) short-circuit with
If-None-Match: */X-If-Absent: trueand probe withHEADbefore writing — the dedup fast path is a first-class API.
3. Architecture & High-Level Design
- Adapter trait —
put/get/delete/list/exists(+ defaultcopy/move_objectcomposition); the local FS backend is the v1 implementation; S3-style adapters can override with native HEAD. - Object API — PUT (idempotent options), GET, HEAD (existence + size without payload), DELETE, list by prefix, copy/move.
- Signed URLs — token-embedded (not path-embedded) authorization: the token carries key + expiry + operation; the handler verifies against the current secret and serves/uploads.
- Env-scoped RLS — bucket/object metadata tables carry
env_id; the tenant context is set on the same connection inside the same transaction.
4. Key Traits and Data Structures
StorageAdapter— the backend boundary: asyncput/get/delete/listwith defaultexists(0-byte read probe; native HEAD adapters override) and defaultcopy/move_object(read-then-write).StorageError—NotFound,PathTraversal(rejected before any IO),Io.UpsertSecretRequest-style object contracts — PUT with content-type + optional idempotency hints; GET returns bytes.- Signed URL tokens — HMAC-signed, time-boxed, operation-scoped (GET/PUT), single-key scope.
- Bucket records — per-env bucket metadata (name, created_by, quota).
5. Rationale, Trade-offs, and Alternatives
- Adapter trait over a fixed backend. The local FS backend ships v1; S3-compatible object stores are an adapter away (native HEAD and range reads override the defaults). Direct object-store access from services would fork credentials and bypass the tenant checks.
- Env-scoped buckets, RLS-enforced. Bucket keys derive from the
env; the RLS policy matches
env_idso even a buggy client cannot read across tenants. ExplicitWHERE env_idclauses remain the primary filter. - Path traversal rejected pre-IO. Key normalization happens before
any file operation —
..components, absolute paths, and URL- encoded traversal are rejected. Post-IO containment (chroot-style) would still leak existence probes. - Idempotent PUT with conditional hints. Content-addressed
verticals probe (HEAD/
exists) then write withIf-None-Match: *; the dedup fast path avoids double-uploading immutable blobs. Unconditional PUT would rewrite identical objects (wasted IO + egress on remote backends). - Signed URLs as time-boxed capabilities. A browser upload needs write access for one object for minutes — a scoped token beats proxying the bytes through the BFF (bandwidth) or granting the browser a token (credential exposure).
- Local FS backend first. Single-binary deployments (the platform's default posture) need no external object store; the adapter keeps the S3 path open without depending on it.
- HEAD-style existence probe.
existsavoids reading payloads — the fast path every content-addressed vertical uses before writing; a read-then-decide flow would ship every byte twice. - Rejected: per-vertical storage silos. Same isolation argument as metrics — one plane, one credential owner, one audit surface.
- Rejected: unauthenticated direct object paths. Without signed URLs, every object read is token-checked; the capability token restores convenience without the permanent exposure.
6. Internal Implementation & Invariants (Private Design)
- Invariants:
- Every object key is validated against bucket-root containment before IO (traversal rejected with a typed error).
- The tenant context is set on the SAME connection, inside the same explicit transaction as every metadata query — a bare-pool SELECT returns 0 rows under FORCE RLS (surfaced historically as spurious 404 "environment not found").
- Authorization still resolves + validates the effective env even on endpoints with explicit path envs — the token's env and the requested env must agree (or carry validated cross-env RBAC).
- Signed URLs expire; expired/unknown tokens are rejected before any storage IO.
- Copy/move preserve content-type metadata; move is copy-then-delete (no partial state left on failure — the delete happens after the successful copy).
- Only
sentin8:storage:*scoped workloads touch objects; the admin UI is Shield-gated with the enforced scope-mirror set.
- Boot (shell-owned). The shell owns the listener stack; Storage builds its capped pool, consumes the Shield engine, initializes the bucket directory layout per env, and mounts the object API + admin UI (+ shared policy-admin surface).
- Storage layout. Objects live under
{data_dir}/{bucket}/{key}; the backend registry (buckets table) maps bucket → env; keys are percent-decoded exactly once and re-validated.
7. Unresolved Questions / Future Designs
- Multipart uploads ride the storage design's presigned-PUT pattern (N presigned part-PUTs + a commit endpoint).
- The S3-compatible backend adapter implements
S3Adapterbehind the adapter trait (deliberate future work per storage design §7). - Per-bucket quota enforcement tracks bytes-per-namespace in the metadata (the dedup path already scans sizes).
- Object versioning is a non-goal: the copy-then-delete contract plus the audit journal cover the requirement.
Design attribution: sentin8:storage and its underlying concepts were designed by Hamid Jahanzad Hamid@Jahanzad.com.