The content service leans on three pieces of shared infrastructure: service-to-service authentication, a Redis read cache, and S3/MinIO object storage. Each is wrapped in its own adapter so the rest of the code talks to a small interface, not to the vendor SDK.
Service authentication
Every inbound HTTP request is treated as a call from another internal service, not from a browser. A Fastify plugin installs an onRequest hook that runs before route handlers. The hook expects an Authorization: Bearer … header; a request with no bearer token, or with an unverifiable one, is rejected with 401 Unauthorized and a short reason code (missing_token / invalid_token). A verified token attaches a small auth context to the request — the calling service name plus its claims — which handlers can read.
The token is a compact HMAC-signed JWT carrying a service name and standard issued-at / expiry timestamps, verified against a symmetric secret held in the service's environment. Verification checks the signature and that the token has not expired. The same module can both sign and verify, so one service mints a short-lived token (default one hour) and its peer checks it.
Health-check routes are exempted by default, and callers can pass extra ignore paths at plugin registration. The match is prefix-aware: an ignored route also covers its sub-paths and query-string variants.
Redis content cache
A ContentCache wraps the Redis client and caches only public reads — published posts (by id, by slug, by alias) and the category tree / list. Drafts are never cached; the setter for a post short-circuits when the item is not published, so unpublished content can never leak from the cache. Keys are namespaced by purpose (a per-lookup prefix for posts, fixed keys for the category tree and list). Posts expire after five minutes, categories after ten.
The consistency rule is asymmetric by design:
- Reads fail soft. A Redis error or timeout, after a couple of short retries, returns
nullso the caller falls back to the database. A cache outage degrades latency, not correctness. - Writes fail loud. Cache sets and invalidations throw on failure after their retries, so a half-applied cache state surfaces rather than hiding.
Every mutation invalidates what it touched: creating, updating, deleting, publishing, or unpublishing a post deletes that post's id/slug/alias keys, and any category change clears the category keys. The cache is a derived copy; the database remains the single source of truth, and write-time invalidation keeps the copy from going stale.
The Redis client itself is a lazily-created singleton built from a connection URL in the environment, with lifecycle logging (connect, ready, reconnecting, close, end) and a clean shutdown path.
S3 / MinIO storage
Media lives in object storage behind an S3StorageAdapter that implements a small StoragePort — the application depends on the port, not on the AWS SDK. The adapter is configured with an endpoint, bucket, region, public base URL, and credentials, and runs in path-style addressing so the same code works against MinIO in development and S3-compatible storage in production.
It handles its own provisioning: ensureBucket creates the bucket if a head request finds it missing, and ensurePublicReadPolicy merges public-read statements into the bucket policy for the prefixes that hold public assets, adding only the statements that are not already present. Uploads land under prefixed, collision-resistant paths — featured images under an images/ prefix, attachments under attachments/, each with a random id segment — and return both the stored key and a public URL composed from the configured base. Downloads and deletes address objects by that key, and a missing object reads back as null rather than an error.
How they fit
These three adapters share a shape: a vendor-specific implementation behind a narrow interface, with failure semantics chosen for the job. Auth fails closed (no token, no entry). The cache fails open on reads and closed on writes. Storage is idempotent about its own setup. The service's own logic stays unaware of Fastify hooks, Redis retries, or S3 bucket policies.