Philosophy
The four principles kreogen is built on, and what they cost.
kreogen exists to solve one problem: an agency starts a lot of projects, and most of the work in the first fortnight of any of them is the same work. Authentication, billing, email, deployment, CI. Doing that again per engagement is not craft, it is overhead the client pays for twice.
So the template is opinionated on purpose. One auth stack, one database, one analytics pipeline, one way to deploy — so that moving between client projects costs nothing, and so that a fix found on one project is a fix on all of them.
Four principles decide the arguments.
Self-hostable
Every integration runs on infrastructure we control, or degrades cleanly when it is absent. No component requires a specific host.
That rules out a lot of otherwise good tooling. Anything whose local development story is "deploy it and see", or whose analytics only report when the app runs on one particular platform, is out — not because those products are bad, but because a client project that cannot be moved is a client project we have made a decision on their behalf about.
The practical test: docker build --build-arg APP=app produces an image, and
that image runs anywhere a container runs. Postgres, Redis and object storage
are protocols, not products, so the local docker compose stack and the
production one differ by connection string.
What it costs. More configuration than a platform that guesses. You write
a Dockerfile argument and a compose file instead of connecting a git
repository to a dashboard.
Typed end to end
From the database schema through to environment variables, validated at boot rather than discovered in production.
Prisma generates types from the schema; Zod validates every boundary the type system cannot reach — environment variables, form input, request bodies, webhook payloads. A server action's TypeScript signature is a compile-time fiction from the caller's point of view, because Next gives every action a stable id and the browser can POST to it directly. So actions parse their input before the handler runs, and the handler's argument type comes from the schema rather than from a hopeful annotation.
Configuration gets the same treatment. Each package declares the variables it needs and each app composes them, so a malformed value fails at startup with a message naming the variable, instead of surfacing three weeks later as a confusing runtime error in one code path.
What it costs. Adding a variable means editing two files, and a schema change means a migration. Both are the point.
Boring to operate
One Docker image per app, one pipeline, one command to bring the whole stack up locally.
Operational novelty is a tax that comes due at the worst possible moment. The pipeline has five stages, the images are tagged by commit sha so a rollback is pointing at the previous one, and health and readiness are separate endpoints because they answer different questions — liveness that touches the database turns a database outage into a restart loop.
The same instinct rejects things that look clever. In-app cron fires once per replica. A logging library with its own transport adds a hop that nothing reads, when the container already hands stdout to a collector. Scheduled work belongs in a scheduled pipeline, where runs are logged, retryable and alertable.
What it costs. Fewer magic conveniences. Nothing here auto-detects your environment, because auto-detection is the thing that fails silently.
Honest defaults
A missing key disables a feature; it never fails silently or half-works.
Only DATABASE_URL and BETTER_AUTH_SECRET are required. Every other
integration client is undefined until you supply its key, so the repository
runs before you have a single vendor account.
The important half of that principle is the second clause. "Degrades cleanly" does not mean "returns quietly" — it means the behaviour is truthful about itself. A contact form whose mail provider is unconfigured must not accept a message and say "we'll be in touch"; a Stripe webhook handler with no key must answer 503 rather than 200, because 200 tells Stripe the event was handled and stops the retries that would have delivered it once the key was fixed.
The rule of thumb: if nobody is waiting, optional-chain it. If somebody is waiting, check and fail loudly.
What this is not
It is not a framework. There is no kreogen runtime, nothing to import, and no
upgrade path that is not a diff you read. kreogen update fetches the
difference between two template versions and applies it, skipping files you
have edited — that is deliberately less magic than a dependency bump, because
a template you have customised is code you own.
It is not a product with a free tier to grow out of. It is the repository Kreotic starts engagements from, published because there was no reason not to. When an engagement proves a decision wrong, the template changes.