Datastores
Your app never picks a database, and never invents an admin login. It declares a capability — “I need a relational database” — and Zhenfy One provisions one, isolates it to your project, and injects its connection details as environment variables. Which engine backs each capability is our choice, and it is the part that can change.
What each capability runs on today
This table is the current answer, not a contract. The capability and the environment variables are the contract; the engine column can change without your app changing.
| Capability | Engine today | Env contract | Reach for it when |
|---|---|---|---|
| relational database | PostgreSQL | POSTGRES_* | the default for anything you need to keep |
| cache | Valkey (Redis API) | CACHE_* | sessions, counters, rate limits — never the source of truth |
| work queue | Valkey Streams | QUEUE_* | decoupling a slow job from a request |
| event stream | Kafka | KAFKA_* | replayable, ordered, multi-consumer events |
| search index | OpenSearch | SEARCH_* | relevance ranking and faceting beyond Postgres full-text |
| analytics store | ClickHouse | OLAP_* | aggregation over ~100M+ rows |
| object storage | Amazon S3 | BLOB_* | files, images, uploads |
| AI model access | Amazon Bedrock | AI_* | an LLM / generative-AI capability your app calls at runtime — Zhenfy does not host GPU inference |
| admin sign-in | Zhenfy account | AUTH_* | an owner-only surface — never an admin password |
| user sign-in with Zhenfy | Zhenfy account | AUTH_TEST_USER_* | your brief asks for every user to sign in with their Zhenfy account — the same handoff as admin sign-in, any verified Zhenfy identity accepted; three beta-only test identities for the suite |
| seeded member accounts | Zhenfy One | SEED_MEMBER_* | invite-only member accounts — two ordinary members the app creates at boot; visible and rotatable in the Admin tab |
Your code stays portable
Nothing Zhenfy-specific goes into your repository. Your app imports the ordinary driver for the engine, reads the documented environment variables, and owns its own schema — there is no SDK to import and no platform package to depend on. A generated Node app that uses a relational database contains roughly this, and nothing else:
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.POSTGRES_URL });Point those variables at any PostgreSQL and the same code runs unchanged — on your laptop, on another host, or anywhere you take the repository.
Where the declaration lives
The request for a store is pipeline metadata, not source code, which is what keeps the repository self-contained. It looks like this:
[{"kind": "database", "name": "main"},
{"kind": "cache", "name": "sessions"}]Note that it names a kind, never an engine. You can see what your project ended up with under the Admin tab, and the Architecture section of a build’s readout reports the stores your app can actually reach.
Admin access, and what stays yours
Two different things get called “auth”, and Zhenfy manages only one of them.
Your app’s admin surface — the owner-only dashboard, moderation, settings — is signed in with your existing Zhenfy account, the same shape as “Sign in with Google”. Declare admin-auth and the platform injects a verification contract: AUTH_LOGIN_URL, AUTH_PROJECT_ID, AUTH_JWKS_URL (a public key set, so there is no shared secret), AUTH_ISSUER, and AUTH_ADMIN_SUBJECT. There is no admin password anywhere — nothing to leak, default, or forget to rotate.
The handoff is four steps, and your app performs them with an ordinary HTTP client:
- An unauthenticated admin arrives; your app redirects the browser to
AUTH_LOGIN_URLwith its ownredirect_uriandAUTH_PROJECT_ID. - Zhenfy signs them in — asking for a password only if they are not already signed in.
- The browser comes back to your
redirect_uriwith a short-livedzhenfy_admin_token. - Your app verifies it (RS256 against
AUTH_JWKS_URL, audienceAUTH_PROJECT_ID, subject equal toAUTH_ADMIN_SUBJECT) and issues its own session.
The audience is what keeps projects apart: a token minted for one app is refused by every other, and it is delivered only to that app’s own address. The subject is your account id rather than your email, so changing your email never locks you out of your own admin surface. Redirects are restricted to the domains your projects are served at, so the token cannot be pointed somewhere else. And the code that does all this is ordinary JavaScript with no Zhenfy import — the repository stays portable.
Your app’s own users are a different matter and stay yours to build. A public app must not require a Zhenfy account to sign up, so we do not make it one unless your brief asks for it. Your app owns those accounts, and the security suite holds them to the usual bar: hashed passwords, no account enumeration, server-side session revocation.
When your brief asks for your users to sign in with their Zhenfy account — “Sign in with Zhenfy” for everyone, no password form of the app’s own — declare user-auth. It is the same handoff as the admin surface, with one difference at the last step: your app accepts any verified Zhenfy identity (RS256 against AUTH_JWKS_URL, audience AUTH_PROJECT_ID, and a stage claim that matches the environment the app runs in) and keeps a user record keyed on the token’s subject rather than its email. The Zhenfy account is the whole credential: your app stores no password, and no route other than /auth/callback ever creates a user or a session. In beta the platform supplies three test identities — AUTH_TEST_USER_TOKEN, AUTH_TEST_USER_TWO_TOKEN and AUTH_TEST_USER_THREE_TOKEN, each a signed token for a different Zhenfy test user — so the acceptance suite can be three different users: one who owns some data, one it is shared with, one it is not. Those names belong to the suite alone; app code that reads them fails the build. Production holds none of them, and a beta token presented to the production app is refused by its stage claim. An app with an owner-only surface declares admin-auth as well — the owner check stays that capability’s — and never seed-member, because Zhenfy users are not accounts your app creates.
When only you can create those accounts — invite-only members, no sign-up — nothing in a test could ever register one, so your project declares seed-member and the platform mints two ordinary members for it: SEED_MEMBER_USERNAME/SEED_MEMBER_PASSWORD and SEED_MEMBER_TWO_USERNAME/SEED_MEMBER_TWO_PASSWORD. Your app creates those two ordinary members at boot through its own password hashing, the acceptance suite signs in as them, and they carry no admin rights. They exist in beta only, where the suite runs; production holds no seeded account. The two member-… rows you see in your app’s own account list are these. The Admin tab shows each username, reveals the password on an audited click, and rotates it — rotation restarts the app so it picks the new password up; sessions the member already holds stay valid until they expire or log out, unless your app revokes them when it restarts. Deleting a seeded member inside your app brings it back at the next restart, so rotate it from the Admin tab instead.
One rule the platform enforces rather than suggests: a credential must be read from the injected environment and fail closed when it is absent. A default, a fallback, or a value assembled at runtime to hide the literal fails the build before any test runs.
Isolation and durability
- Each project gets its own database, its own role, and its own key/topic/index prefix on a shared engine.
- Beta and production get separate stores, so a beta experiment cannot touch production data.
- Data written to a declared store survives restarts, redeploys, and promotion to production.
- An app that declares no store keeps its data on the container’s disk, which is erased on every restart.
- Deleting a project deletes its stores and their contents.