All projects

Cacheify

Live

Managed, multi-tenant Redis-compatible cache with enforced tenancy.

Role
Product engineering, full-stack, platform and protocol design

Problem and users

Redis is excellent at what it does, but using it safely is still a platform problem.

A small team that needs sessions, rate-limit counters, OTPs, feature flags, queues or leaderboards has three options:

  1. Run Redis themselves — provision a server, harden it, configure ACLs, monitor memory, and remember that expiry is a convention rather than a guarantee.
  2. Buy managed Redis — still server-shaped: pick a region, wire a VPC, manage credentials, and own the bill for capacity you may not use.
  3. Build a cache abstraction in-house — usually a thin wrapper around a client, with no tenancy, no quotas, no audit trail, and no consistent expiry policy.

None of these solves the actual job: give an application a safe place to put short-lived data, with a credential that cannot escape its database and a guarantee that forgotten keys eventually disappear.

Cacheify is built around that job.


What I built

A developer can go from zero to a working cache in four steps:

  1. Create an account and verify a 6-digit email code.
  2. Create a database — five are free, each fully isolated.
  3. Mint its single active API key — read-only or read-write; the plaintext is shown exactly once.
  4. Add one line to the project:
REDIS_URL=redis://:<rk_key>@<gateway-host>:<port>/0

Any native Redis client then works unchanged: redis-cli, ioredis, redis-py, Jedis, Lettuce, Spring RedisTemplate, go-redis. The same data is also reachable over JSON/HTTPS for serverless functions, browsers, automation and AI agents.

Underneath the simple surface, the platform adds what raw Redis does not:

CapabilityWhat it means
Tenant isolationEvery database is a sealed namespace. Cross-tenant access returns 404, not 403, so the API never confirms another account's database exists.
Scoped credentialsExactly one active key per database, read-only or read-write. Writes from a read-only key are refused before execution.
QuotasDatabase count, keys per database and value bytes. Over-quota writes roll back instead of partially landing.
FairnessPer-IP/user rate limits plus per-database in-flight caps. Saturation returns 429 + Retry-After instead of holding sockets.
Mandatory expiryEvery write gets a TTL — 24 hours by default. PERSIST is refused on both transports.
AuditabilityRequest IDs, structured logs, audit events for key/database lifecycle, and Server-Timing on responses.
AdministrationSearch users, edit quotas, suspend/reactivate, promote/demote, inspect usage.

What it is not

Cacheify is not a full Redis replacement. Transactions (MULTI/EXEC/WATCH), pub/sub, blocking commands and Lua scripting are deliberately outside the multi-tenant surface.

That is a product decision, not an omission: those features complicate isolation, fairness and support in ways that outweigh their value for the target use cases. The docs state the boundary plainly rather than letting users discover it in production.

Who it is for

  • Indie developers and small teams who need a safe cache without operating Redis.
  • Serverless and AI-agent workloads that cannot hold a long-lived Redis socket and prefer HTTPS/JSON.
  • Operators who want to run an internal Redis-compatible platform on their own hardware, with every team isolated behind its own database and key.

1. Onboarding and identity

Registration hashes the password with bcrypt, creates an unverified account, and returns a session token. The account is gated until email verification:

  • a 6-digit OTP, hashed with a server-side pepper,
  • a 10-minute TTL,
  • a 60-second resend cooldown,
  • a maximum of five attempts.

In remote mode, OTPs are stored in the platform's own Redis namespace — the product dogfoods its data plane. In embedded development mode, a small in-memory store with the same semantics is used.

Sessions are compact HMAC-signed tokens, not library-issued JWTs. The payload carries a token_version; suspending an account or changing a role rotates that version, invalidating existing sessions immediately. Login always runs one bcrypt comparison — against a dummy hash when the email does not exist — so response timing cannot be used to enumerate accounts.

2. Database and key lifecycle

Creating a database:

  • checks the plan quota,
  • enforces a unique name per owner,
  • assigns a random 12-character namespace.

Minting a key:

  • generates 24 random bytes,
  • stores only the SHA-256 hash and a short display prefix,
  • returns the plaintext exactly once.

A database can have one active key. A second mint returns 409; rotation is revoke-then-mint. Revocation bumps an in-memory cache epoch so the next request re-consults the store immediately.

Databases can be archived (read-only) and restored. Deleting a database removes its keys first, then its metadata, and fails the delete if remote cleanup fails — tenant data must never outlive its registry row.

3. The command path

Every data command follows the same sequence:

  1. Resolve identity — session token or rk_ API key.
  2. Resolve scope — the key's database and permission level.
  3. Authorize — owner check, read-only rejection for writes, archived-database rejection.
  4. Preflight quotas — would this command create new keys beyond the limit? Is the value too large?
  5. Execute — embedded engine or remote Redis, with key names rewritten into the tenant namespace.
  6. Post-check and persist — value-size check, snapshot restore on failure, TTL stamping.
  7. Respond — { success, data, meta }, request ID, Server-Timing, audit/usage side effects.

The console is not privileged. The key browser, key editor and command console call the same endpoints an application calls; they simply authenticate with a browser session instead of an API key. A bug reproduced in the console is a bug in the real product path.

4. Embedded engine

The embedded engine stores typed values as JSON documents in MongoDB:

  • string, list, set, hash, zset,
  • WRONGTYPE semantics,
  • atomic counters,
  • glob matching for KEYS and SCAN,
  • lazy expiry on read plus a background sweeper,
  • snapshot/restore for quota rollback.

It is the zero-configuration default: no external Redis to run, and the same command contract as remote mode.

5. Remote Redis mode

When an operator configures a real Redis server, every hosted database becomes a namespaced tenant:

rh:<namespace>:<key>

The driver:

  • rewrites key arguments at the boundary,
  • rewrites KEYS/SCAN patterns in both directions,
  • re-implements FLUSHDB as a bounded SCAN + UNLINK loop,
  • uses MULTI/EXEC for pipelines when the provider supports it,
  • falls back to sequential execution when it does not,
  • stamps default TTLs on newly created keys,
  • uses MEMORY USAGE where available and degrades to unknown size otherwise.

TLS uses rediss:// with a provider CA bundle; certificate and hostname verification are never disabled.

6. RESP gateway

The gateway is a single stdlib-only Node process that speaks RESP2 and RESP3:

  • incremental parsing across arbitrary TCP chunk boundaries,
  • AUTH accepts the rk_ key as the password,
  • HELLO negotiates RESP3,
  • the first data command calls /api/auth/whoami to resolve the key's own database,
  • SELECT 0 is virtual; other indexes require an operator registry,
  • PING, CLIENT, CONFIG, COMMAND and INFO are answered locally so client libraries boot cleanly,
  • pipelined reads collapse into one /pipeline round-trip,
  • concurrency is capped globally and per connection,
  • upstream 429 triggers a global cooldown,
  • there are no silent retries — the client library's retry policy decides.

Because whoami routes each connection to its own database, one gateway process serves every customer instead of being pinned to a single database.

7. Console and admin

The signed-in console includes:

  • database overview with key counts, size and status,
  • split-view key browser and typed key editor,
  • command console against the same data plane,
  • TTL controls and countdowns,
  • database settings: rename, archive/restore, API keys, flush, delete,
  • API-key management with plaintext-once reveal,
  • profile with a seven-day command-usage graph,
  • admin control plane: user search, quota editing, suspend/reactivate, promote/demote.

The admin surface is separated by role and protected on the server, not merely hidden in the UI.

8. Docs, blog and machine-readable surfaces

  • 8-page docs section: overview, quick start, Redis gateway, HTTP data plane, architecture, TTL policy, errors, FAQ.
  • 5 engineering blog notes: HTTP vs RESP, mandatory TTL, one key per database, 404 vs 403, the console as a client, quotas that roll back.
  • OpenAPI 3.1 and /llms.txt for tooling and AI agents.

The docs are part of the product, not an afterthought. They explain trade-offs and boundaries, which is exactly what a reviewer or interviewer will probe.


Architecture

The README's shorthand is the best one-line explanation:

Cacheify turns the Redis request path into an API with a bouncer, a meter and a janitor. The bouncer checks credentials and permissions, the meter enforces quotas and rate limits, and the janitor guarantees every key leaves eventually.

The deeper model is two doors, one room:

Native Redis client ──► RESP gateway ──┐
                                       ├──► one policy path ──► tenant namespace ──► storage
Browser / serverless ──► HTTPS JSON ───┘

Five principles fall out of that model:

  • The transport is not the architecture. RESP and HTTPS are two adapters over the same authorization, quota and command service.
  • A database is not a server; it is a namespace with a contract. On a shared Redis it becomes rh:<namespace>:<key>. On the embedded engine it is a scoped set of MongoDB documents.
  • An API key is a capability, not a password. It selects exactly one database and one permission level.
  • The request path is the product. Auth, verification, CORS, body limits, concurrency, rate limits and quotas are evaluated before the engine runs.
  • Expiry is a schema rule, not a cleanup job. There is no state in which a Cacheify key has no TTL.

This model explains nearly every implementation decision in the codebase, and it is the right thing to lead with in an interview.


┌─────────────────────────────────────────────────────────────────────┐
│  consumers                                                          │
│  desktop Redis clients (RESP)          browser + HTTP services      │
│        │                                       │                    │
│        ▼                                       ▼                    │
│  ┌──────────────┐                    ┌──────────────────┐           │
│  │ RESP gateway │  translates Redis  │  HTTPS data plane │           │
│  │ (redis://)   │  commands to REST  │  (JSON over TLS)  │           │
│  └──────┬───────┘                    └────────┬─────────┘           │
│         └──────────────┬──────────────────────┘                     │
│                        ▼                                            │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ control plane: auth, permissions, rate limits, quotas,      │    │
│  │ validation, audit trail                                     │    │
│  └───────────────────────────┬─────────────────────────────────┘    │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ tenant namespaces — every database is a sealed keyspace     │    │
│  │ (rh:<namespace>:<key>) on the backing store                 │    │
│  └───────────────────────────┬─────────────────────────────────┘    │
│                              ▼                                      │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ data plane engines: embedded engine (default) or a real     │    │
│  │ Redis server the operator configured                        │    │
│  └─────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘

Layer responsibilities

LayerResponsibility
TransportsRESP gateway for native clients; JSON/HTTPS for browsers, scripts and agents
Control planeIdentity, email verification, API-key scope, permissions, quotas, rate limits, validation, audit
TenancyA database is an isolated keyspace; the API key pins the caller to one database and one permission level
Data planeEmbedded Redis-semantics engine over MongoDB, or a real Redis server via ioredis
ExperienceMarketing site, docs, blog, developer console, admin control plane

The key architectural invariant: both transports end in the same service layer. There is no second implementation of authorization or quota logic for the gateway. That is what keeps the product honest.


Architecture graph

Nodes

  • Desktop Redis clients
  • Browser and HTTP services
  • RESP gateway
  • HTTPS data plane
  • Control plane
  • Tenant namespaces
  • Embedded engine
  • Remote Redis server

Edges

  • Desktop Redis clients to RESP gateway — RESP commands
  • Browser and HTTP services to HTTPS data plane — JSON over TLS
  • RESP gateway to Control plane — translates commands to REST
  • HTTPS data plane to Control plane — JSON requests
  • Control plane to Tenant namespaces — auth, quotas, audit
  • Tenant namespaces to Embedded engine — default
  • Tenant namespaces to Remote Redis server — operator configured

Key decisions and tradeoffs

The job to be done

"Give me a safe cache in one line, without provisioning a server, writing an ACL policy, remembering a TTL convention, or building an admin UI."

Differentiation

Redis protocol and data structures are commodities. The defensible part is the policy layer and developer experience:

  • isolation,
  • scoped credentials,
  • quotas,
  • mandatory expiry,
  • a console,
  • docs,
  • and a gateway that works with the clients people already use.

Why the free plan is coherent

Five databases, 10,000 keys, 512 KiB values and a 24-hour default TTL are not just marketing numbers. They are cost controls.

On shared infrastructure, the most expensive key is the one nobody remembers writing. Mandatory expiry plus quotas makes the worst-case storage bound predictable. A free tier without those controls would either degrade for everyone or require manual cleanup.

Why "bring your own Redis" matters

It turns the product into an internal developer platform. A company can run Cacheify against its own Redis server and give teams isolated, governed databases without giving them raw server credentials. That is a credible B2B angle beyond the free consumer plan.

The one-key decision

Fewer credentials to leak, a clear rotation path, and an inventory that matches the database inventory.

The cost is no overlapping key generations during a rolling deploy. That is a real operational constraint, and the docs say so. It is a good example of a product decision that trades convenience for blast-radius control.

The biggest risk

"Redis-compatible" invites comparison with full Redis. The honest boundary — 60 commands, no transactions/pub-sub/scripting — is the right mitigation.

Target use cases:

  • session stores,
  • rate-limit counters,
  • feature flags,
  • queues,
  • leaderboards,
  • OTPs and idempotency keys.

Not durable primary storage.

A timely angle

The HTTP plane plus OpenAPI and /llms.txt makes the platform usable by AI agents and serverless runtimes that cannot hold a Redis socket. That is a genuine distribution advantage, not a buzzword.


Hard parts

1. Standard Redis clients vs. a JSON API

Problem: Native Redis clients speak RESP over TCP; the platform's data plane is JSON over HTTPS. Asking users to install a driver or write an HTTP bridge would kill the "one line" promise.

Solution: A RESP gateway that translates every command onto the existing REST contract. AUTH carries the API key; whoami routes the connection to the key's database; SET without a TTL is rewritten to include PX; EXPIRE maps to the TTL endpoint; everything else passes through to /command.

Trade-off: The gateway is another process to deploy, and it must track Redis protocol quirks. But it preserves one authorization and quota path, instead of creating a second data plane that could drift.

2. Tenant isolation on one shared Redis server

Problem: A shared Redis server has no per-tenant auth in many managed offerings. One customer must never see another's keys.

Solution: Rewrite key arguments at the driver boundary into rh:<namespace>:<key>. Rewrite KEYS/SCAN patterns in both directions. Re-implement FLUSHDB as a bounded SCAN + UNLINK loop so it can never touch another namespace. Delete the namespace before deleting the metadata row, and fail the delete if cleanup fails.

Trade-off: This is application-level isolation, not Redis ACL isolation. Anyone with direct server credentials can see all namespaces. The docs state this honestly. It also means KEYS is materialized over SCAN, because restricted provider users are often denied the blocking KEYS command.

3. Redis semantics without Redis

Problem: The default mode must work with zero external dependency, but the product promises strings, lists, sets, hashes, sorted sets, TTL, atomic counters, WRONGTYPE, glob matching and pipelines.

Solution: A command registry with argument validation and per-type handlers, storing typed values as JSON documents in MongoDB. Expiry is evaluated lazily on read, with a background sweeper for accumulation. Quota failures restore a snapshot of the affected keys — the atomicity the old transactional store provided.

Trade-off: No multi-document transactions. The snapshot/restore path is the substitute; it costs memory and latency proportional to the affected keys. Remote mode gets native Redis semantics and uses MULTI/EXEC when the provider supports it.

4. Mandatory expiry without breaking Redis clients

Problem: Redis users expect SET without a TTL to create an immortal key. The platform's business model depends on the opposite: no forgotten key may become permanent cost.

Solution: Enforce the rule at every write path — SET, MSET, INCR, APPEND, LPUSH, RPUSH, SADD, HSET, ZADD. Plain SET is rewritten to carry PX; created names get a PEXPIRE after collection writes. PERSIST is rejected at the service and in the gateway, surfacing as a Redis error. Legacy immortal keys were swept retroactively.

Trade-off: It surprises users who expect Redis exactly. The docs confront it directly, the console shows remaining TTL, and TTL/PTTL always return a finite value — so the behavior is at least predictable.

5. Latency: every command paid for the control plane

Problem: A single /command could do 6–8 serial MongoDB round trips: resolve the API key, resolve the database, load the owner's quotas, read the key row, update usage. On a remote cluster, that dominates latency.

Solution: A 30-second in-process cache for key scope and database records, with epoch invalidation — any control-plane change (revoke, rotate, suspend, quota patch, archive) bumps the epoch and clears the cache instantly. Usage tracking is fire-and-forget. Server-Timing attributes time to engine and redis phases. A dedicated Mongo latency probe separates storage RTT from query-count cost, so the fix is chosen with data rather than guesswork.

Trade-off: Cached records can be up to 30 seconds stale; the epoch mechanism removes that window for the operations that matter. The caches are per-process, so horizontal scaling needs a shared invalidation channel.

6. Trust: credentials, enumeration and error semantics

Problem: A multi-tenant cache is an attractive target. Leaking existence, timing or plaintext credentials is a product-level failure, not just a bug.

Solution: Store only API-key hashes; show plaintext once. Return 404 for cross-tenant resources so the API never confirms another tenant exists. Use 403 only where the caller legitimately knows the resource exists — a read-only key attempting a write. Equalize login timing with a dummy bcrypt comparison. Hash OTPs, bound attempts and cooldowns. Use an exact-match CORS allowlist. Redact sensitive fields in structured logs. Record audit events with request IDs.

Trade-off: The one-key-per-database model makes rotation a hard cutover: revoke, then mint. The docs make that explicit. It is a deliberate blast-radius decision, not an oversight.

7. Deployment reality

These are the unglamorous problems that make a project credible:

  • Proxy 503: the API initially bound to loopback, so nginx could not reach it. Fix: bind 0.0.0.0 by default and honor the panel-injected SERVER_PORT.
  • MongoDB SRV failures: home routers and ISP resolvers sometimes refuse SRV queries. Fix: detect the failure once, switch to public DNS resolvers, and retry with a clear error if it still fails.
  • TLS: remote Redis uses rediss:// with a provider CA bundle; certificate and hostname verification are never disabled.
  • Repository hygiene: an unanchored data/ ignore rule swallowed the backend/server/modules/data source directory. Fix: anchor the rule.
  • Dependency security: nodemailer was bumped across major versions to clear a batch of advisories; the audit is now clean.
  • Gateway packaging: a small .env loader reads the file next to the binary, so panel/npm start deployments behave like local runs; the Docker image runs as non-root with a healthcheck.

8. Product clarity: two transports, one story

Problem: RESP and HTTP could easily become two products with two sets of docs and two mental models.

Solution: Make the redis:// connection string the primary integration story; present the HTTP plane as an advanced reference, not a second door users must choose. Keep a single docs registry so navigation, pager and links never drift. Publish OpenAPI 3.1 and /llms.txt for tooling and AI agents. State the unsupported surface — no transactions, pub/sub, blocking commands or scripting — instead of letting users discover it in production.

Trade-off: The platform will lose users who need full Redis. It gains trust from the ones who fit the 60-command subset.


Results

Testing

SuiteWhat it proves
39-step smoke suiteRegistration, verification, login, CORS, cross-tenant 404, database CRUD, all five data types, TTL policy, quotas, API-key scope, read-only enforcement, revocation, suspension with token rotation, admin authorization, rate limiting, pipeline atomicity, cascade deletes
7-framework client kitNode, Python, PowerShell, cURL, Java, Go and Spring Boot all exercise the same scenario with their own HTTP clients and Redis idioms
RESP wire suitesEmbedded and live-environment tests for AUTH, HELLO, RESP2/RESP3, SET/GET, TTL, pipelining, WRONGTYPE, KEYS, SELECT, multi-database routing and customer isolation
Stress harnessConfigurable single-command and pipeline load modes, with throwaway accounts and cleanup

The polyglot client kit is the strongest proof of the integration promise: the same contract works whether the caller is a browser, a Python script, a Go service or a Spring application.

Observability

  • every request carries an X-Request-Id,
  • structured JSON logs redact passwords, tokens, API keys, cookies and authorization headers,
  • Server-Timing exposes total time and named phases such as engine and redis,
  • audit events record key and database lifecycle changes,
  • rate-limit headers are returned on every limited route.

Security posture

  • HMAC-signed sessions with token-version rotation,
  • bcrypt password hashing with timing-equalized login,
  • SHA-256-hashed API keys, plaintext shown once,
  • OTP hashing, cooldown and attempt limits,
  • exact-match CORS allowlist,
  • 404 cross-tenant contract,
  • body-size and concurrency limits,
  • TLS with CA pinning for remote Redis,
  • non-root gateway container,
  • dependency audit kept clean.

Deployment

  • Next.js frontend on Vercel,
  • Express API on a separate host,
  • gateway deployable as a non-root Docker container or via pm2,
  • fail-fast environment validation in production,
  • graceful shutdown with a hard timeout,
  • healthcheck that uses pre-auth PING and never touches data.

AreaWhat it demonstrates
backend/server/modules/data/service.jsAuthorization, quota preflight, rollback, TTL policy, pipeline execution
backend/server/modules/data/remote-driver.jsNamespace isolation, key/pattern rewriting, scoped FLUSHDB, MULTI/EXEC, TLS
backend/server/modules/data/engine.js60-command registry and Redis-semantics surface
backend/server/modules/data/key-store.jsMongoDB key store, lazy expiry, snapshots, mandatory TTL
backend/server/core/middleware.jsAuth resolution, API-key cache with epoch invalidation, rate limits, validation, error envelope
backend/server/core/crypto.jsHMAC sessions, API-key generation and hashing, namespaces
gateway/redis-gateway.jsRESP2/RESP3 parser, command translation, pipelining, backpressure, error mapping
frontend/src/components/layout/AuthGate.jsxRoute guards, hydration-safe redirects, onboarding
frontend/src/styles/tokens.cssDesign tokens for the console and marketing site
backend/scripts/smoke-test.js39-step end-to-end policy and data-plane suite
backend/tests/clients/Seven-framework integration kit
backend/scripts/mongo-latency-probe.jsStorage-latency diagnosis behind the per-command optimization

Story

Most portfolio projects are CRUD apps with a login screen. Cacheify is a study in the parts that are easy to underestimate:

  • multi-tenancy,
  • protocol translation,
  • quota atomicity,
  • expiry semantics,
  • latency,
  • backpressure,
  • credential lifecycle,
  • deployment reality,
  • and documentation that tells the truth.

The sentence to remember:

"I built the boring, hard parts that make a shared cache safe: the policy path, the tenant boundary, the protocol bridge, the TTL rule, and the tests that prove they hold."

Stack

LayerTechnologyWhy it fitsTrade-off
FrontendNext.js 16.4 App Router, React 19.3, plain JavaScript, VercelMarketing site, docs, blog and the signed-in console in one deploy. Route groups separate public pages from guarded app pages. Server components for content, client components for the console.No TypeScript, so runtime validation and tests carry more weight.
API / control planeNode.js, Express 5.3, MongoDB driver 7.7, Zod 4.6, bcryptjs, dotenvStateless JSON API with bearer sessions. MongoDB holds users, databases, hashed keys and audit events — and doubles as the embedded key store.MongoDB is not a cache; the embedded engine must recreate Redis semantics.
Data plane — embeddedCustom Redis-semantics engine over MongoDB redis_keysZero external dependency; one store for platform and tenant data; quota rollback through snapshots.No native Redis data structures or expiry; latency is bounded by Mongo.
Data plane — remoteioredis 6 against a real Redis server, rediss:// with CA pinningProduction-grade Redis semantics; operators can bring their own server; the command contract stays the same.Namespace isolation is application-level, not Redis ACL-level.
RESP gatewayNode stdlib only (net/tls), zero npm dependenciesSpeaks RESP2/RESP3 to any client, translates commands onto the existing HTTPS plane, tiny attack surface, deployable as one file or a non-root container.Must implement protocol details and client boot quirks itself.
Validation & securityZod schemas, HMAC-SHA256 session tokens, bcryptjs, SHA-256-hashed API keys, OTP via Brevo/SMTPDeterministic request shapes and error envelopes; no secret material in the browser; token-version rotation on suspension or role change.Synchronous bcrypt can block under heavy auth load.
OperationsDocker (non-root gateway), healthcheck, pm2, fail-fast env validation, graceful shutdownPanel-friendly deployment; the gateway can run at the edge; misconfiguration fails before serving traffic.Mostly single-process assumptions; horizontal scaling needs shared state.
Testing39-step end-to-end smoke suite, 7-framework client kit, RESP wire suites (embedded + live), stress harnessProves the policy path, the wire protocol and the polyglot contract.Embedded and remote paths still need occasional parity checks.

The unfashionable choices are deliberate: MongoDB as the embedded store, plain JavaScript, a hand-written engine, and a stdlib-only gateway. Each buys simplicity or control at the cost of some Redis completeness or raw throughput. The design makes that trade explicit instead of hiding it.