Documentation
CrescoDB is a local-first backend toolkit: a database, an auto-generated REST API, auth with roles, file storage, realtime subscriptions and an admin dashboard, on SQLite, PostgreSQL or MySQL. It runs on your machine, deploys to a server you own, and watches it once it is live.
Install
CrescoDB is a CLI. It needs Node 20 or newer: Node 22 LTS is the safest choice. (CrescoDB uses a native SQLite module that ships prebuilt binaries only for supported Node versions; on anything older the CLI stops with a clear message rather than a compiler error.)
On an older Node, install a current one first. nvm install 22 && nvm use 22 (macOS/Linux), nvm install 22.21.0 && nvm use 22.21.0 (nvm-windows), or grab the LTS installer from nodejs.org.
Quickstart
A new project starts empty: add tables with cresco create, the dashboard's schema editor, or by describing them in plain English. (Want demo tables? cresco init --starter.) Open the dashboard at http://localhost:3000/studio to browse data, edit schema, see a live schema diagram (ERD), run SQL, upload files, and watch changes live.
Your app is served too: cresco dev includes a built-in live server at http://localhost:3000/app: it serves your project folder (your index.html, CSS, JS) and auto-reloads the page whenever you save, so you build your frontend right next to its API. Secrets (config.json, .env, database files) are never served.
Video walkthrough
A full end-to-end walkthrough: starting a project, designing the schema, and deploying it to a real server with a custom domain and automatic HTTPS.
Coming shortly: the walkthrough is being recorded. Until it lands, the written Quickstart and Deploy to a server you own sections cover the same ground step by step.
CLI reference
- cresco login: sign in to your CrescoDB account · logout · whoami
- cresco init: scaffold a new (empty) project · --starter for demo tables
- cresco dev: run the API server + dashboard (-p to set the port)
- cresco start: the production server: API + auth only, no admin surfaces
- cresco generate: sync schema.cresco → tables
- cresco ai "…": natural language → schema
- cresco create <feature>: write a feature as full editable code (auth, blog, payments) · --express / --next
- cresco add <module>: install a managed module: bundled or from the registry
- cresco publish: publish a module from this folder to the registry
- cresco orgs: list your teams · cresco org create <slug> · org add <slug> <email> · org members <slug>
- cresco link: share this project with your team · cresco projects (list) · cresco open <slug> (open a teammate's)
- cresco deploy: prepare this project to run online (Dockerfile + host configs), or push it to one of your servers with --server. Runs the access audit first and refuses on a critical finding · --allow-public to override
- cresco eject: write a folder that runs this project without CrescoDB: your code, your data as plain SQL, the engine vendored, Dockerfile + compose · --out · --db · --force
- cresco audit: access control + dependency vulnerabilities · --access-only · --deps-only. Exits non-zero on a critical finding, so it works in CI
- cresco install <pkg>: add npm packages · cresco fund
The CLI sends its version with every request to the hosted platform. Releases below the supported minimum are refused with a clear upgrade message rather than failing in confusing ways, and npm install -g crescodb@latest fixes it. Nothing is sent for projects running fully offline.
Schema
Your data lives in schema.cresco: JSON models with typed fields (string, text, number, boolean, uuid, date, json). It's the single source of truth; the dashboard and AI edit the same file.
Databases (SQLite · PostgreSQL · MySQL)
SQLite is the zero-config default: perfect for local development and read-heavy apps, with nothing to install. Pick a different engine from dashboard → Settings → Connection → Database engine (or set dialect in config.json). Your schema.cresco and the entire REST API stay identical across engines.
PostgreSQL, two ways, no DBA required
- Zero-config local Postgres. Choose PostgreSQL and leave the connection blank. CrescoDB downloads and runs a real Postgres server for you (data kept in .cresco/pgdata, so it persists). No install, no credentials, just like SQLite. cresco dev prints the local port so you can also connect pgAdmin/psql to it.
- Your own / hosted server. Already have Postgres, a local install, docker run postgres, or a managed host like Railway, Neon, or Supabase? Paste its connection string and CrescoDB connects to that instead (it honors sslmode=require for managed hosts). This is also how a whole team shares one live database.
MySQL, bring your own
Choose MySQL and provide a connection string to a MySQL server (a local install or one line of Docker: docker run -e MYSQL_ROOT_PASSWORD=pass -p 3306:3306 -d mysql).
Switching engines & your data
- Your first switch is safe. Moving off the default SQLite carries your schema over and leaves your existing cresco.db file untouched, nothing is deleted.
- Switching again later doesn't migrate data automatically (engines store data differently). To carry data across a later switch: Export your project as .sql (Settings → Connection), switch, then Import it into the new engine.
- After changing the engine, restart cresco dev to connect.
REST API
Every model gets full CRUD automatically: GET/POST/PUT/PATCH/DELETE /:model, plus /:model/:id, bulk insert, and ?limit/?offset/?where. No routing to write.
Paging
List endpoints take ?limit (default 100, at most 1000) and ?offset, and ?count=1 adds the unpaginated total as an X-Total-Count header. Results are ordered by the primary key unless you pass ?order, so walking pages is stable by default rather than only when you remember to ask.
A ?limit over 1000 is answered with a 400 that says how to page, rather than quietly returning fewer rows. Without a ceiling, one anonymous request on a public table could ask for every row at once and make the server hold them all in memory. To read more than 1000 rows, page with ?after (below) or ?offset. A ?filter=column:in: list takes at most 500 values.
For anything bigger than a page or two, use keyset paging instead. Offset paging asks the database to count rows and skip them, so anything inserted or deleted before your position shifts every later page: walk a table while it is being written and you will see rows twice and miss others, with nothing in the response to tell you. Keyset paging asks for what sorts after a given row, which cannot drift, and is faster because the engine seeks on an index instead of counting past rows it then discards.
Pass ?after=<cursor>. Every full page returns an X-Next-Cursor header; feed it back to get the next one. When the header is absent, you have reached the end.
# first page curl -i "$API/posts?limit=100&order=created_at" → X-Next-Cursor: eyJvIjpbImNyZWF0ZWRfYXQiLCJpZCJdLCJ2IjpbIjIwMj... # next page curl -i "$API/posts?limit=100&order=created_at&after=eyJvIjpbImNy..."
Keep ?order identical for the whole walk. A cursor records the sort it was issued for, so changing it mid-walk is a clear 400 rather than a page of nonsense. The primary key is appended to your sort automatically, so ordering on a column with duplicate values still pages cleanly. ?after and ?offset together is a 400, and you cannot page past a NULL in a sort column, because a null has no defined position relative to a value.
Bulk insert
POST /:model/bulk takes a JSON array and is all or nothing: the whole batch runs in one transaction, so if any record fails, nothing is written. There is no partial-success mode on purpose — a half-applied import that returns 200 leaves you working out what landed.
A failure tells you exactly which record caused it. The response carries the position and the record itself, so a rejected 500-row payload does not need bisecting:
# 409 { "error": "Bulk insert failed at index 47: UNIQUE constraint failed: users.email", "index": 47, "rejected": { "email": "[email protected]" } }
Auth & roles
Two ways to get auth, both real:
- cresco create auth: writes complete auth code you own (register.js, login.js, middleware.js, jwt.js) wired to your project database. Read it, edit it, extend it.
- cresco add auth: enables the built-in managed auth: JWT register/login/me plus role-based access control, roles guest · user · mod · admin and a per-table access policy enforced on every request. Models can also declare a rowOwner column for per-row security: users only ever see and touch their own rows.
Manage users and policies from the dashboard's Users page either way.
Access control
Tables are private by default. A model that declares no access block requires a signed-in user to read it. Nothing is served to anonymous callers unless you say so.
This changed in 1.0. Before that the default was read: ["*"], meaning any model you hadn't thought about was readable by anyone on the internet once deployed. That is the same default behind the wave of AI-built apps found serving their user tables through a public key, and it was wrong. If you have a project from an earlier version, run cresco audit before you upgrade, anything that relied on the old default will now return 401.
The defaults:
- read · create · update: any signed-in user
- delete: admin only
Public is still completely supported, it just has to be a decision you typed. A price list or a blog post should be readable by anyone:
{
"name": "Post",
"access": { "read": ["*"], "write": ["authenticated"], "delete": ["admin"] },
"fields": [ ... ]
}
Roles are guest · user · mod · admin. "*" means anyone with no token; "authenticated" means any signed-in user. Add rowOwner to scope rows to the user who owns them, without it, any signed-in user can read every row in the table, which is right for an internal tool and wrong for a SaaS.
cresco audit
Answers one question offline, from the schema in your repo: who can read this table?
cresco audit # access control + dependencies
cresco audit --access-only # just the access review
It ranks by what is actually exposed, not merely by whether something is exposed. A public table holding an email column is critical. A public price list is a note. It also flags tables holding personal data with no rowOwner, and warns when auth is switched off entirely.
It exits non-zero when it finds something critical, so it works in CI as-is.
cresco deploy runs it first and refuses to proceed on a critical finding: no Dockerfile, no host configs, nothing written. Deploying is the moment a mistake stops being cheap, so that is where the gate lives. If your API really is meant to be open, say so:
cresco deploy --allow-public
Most scanners for this problem work from the outside: you give them a live URL and they probe your tables the way an attacker would. That only helps after you have already shipped the mistake. Because CrescoDB owns your schema, this runs before anything is deployed.
Your AI agent, over MCP
An agent is only as good as the feedback loop it can close. Against a hosted backend it is writing blind: the database is remote, the schema is behind a dashboard, and it cannot call the API it just generated. Here everything is a file on the machine the agent is already sitting on, so it can read the real schema, change it, run the real API and run the real audit.
The server ships inside the CLI. There is nothing extra to install.
--print gives you the config in the shape that client actually wants, and tells you which file it belongs in.
Claude Code
One line, run in the project folder. It writes .mcp.json, which is committed with the repo, so your whole team gets it.
Codex
Codex reads TOML, not JSON. Add this to ~/.codex/config.toml, or to .codex/config.toml in a trusted project. codex mcp add does the same thing interactively.
Cursor
.cursor/mcp.json, under an mcpServers key. Restart Cursor afterwards.
VS Code, Copilot agent mode
.vscode/mcp.json, and the root key is servers, not mcpServers. Getting that wrong writes a valid-looking file that silently does nothing. Needs VS Code 1.99 or newer with agent mode on.
Claude Desktop
claude_desktop_config.json, under mcpServers. Fully quit and reopen the app, not just the window.
The VS Code extension writes the JSON ones for you in one click and keeps the MCP servers you already had. Codex is TOML, so copy the block above rather than letting anything merge it for you.
It speaks standard MCP over stdio, protocol 2024-11-05, so any compliant client works and not only the four above.
The 14 tools
- Read: project_info · schema_read · rows_list
- Verify: access_audit · api_request
- Auth: auth_enable · auth_create_user
- Change: schema_add_model · schema_add_field · schema_set_access · schema_drop_model · db_sync · sql_query · data_export
Worth asking it
- "Read my schema and tell me if anything here is unsafe to deploy."
- "Add a bookings table where a customer can only see their own rows."
- "Create a test user, log in as them, and prove they cannot read anyone else's data."
That last one is the interesting one. The agent creates the second user, calls your real API with their real token, and shows you the empty array. It is not claiming per-user isolation works, it is demonstrating it.
VS Code extension
Install CrescoDB from the VS Code marketplace, or from Open VSX for Cursor, Windsurf and VSCodium, which cannot reach the Microsoft one.
It drives the cresco CLI, so install that first. It only does anything in a project that has a schema.cresco, including one in a subfolder, and it wakes up on its own when you run cresco init.
It is not another database GUI. The dashboard already is one, and Open dashboard opens it in your browser rather than a panel. What the extension does is the part only an editor can:
- Findings on the line. Save schema.cresco and a table anyone can read gets underlined, with the reason in the hover. Same cresco audit that cresco deploy runs, so the editor and the deploy can never disagree.
- Fixes from the lightbulb. Make it signed-in only, scope rows to their owner using a column you actually have, restrict deletes to admins, or swap "public" for the wildcard "*" that actually means anyone. With several criticals, one action fixes them all. Only the access block's bytes change, so your formatting survives.
- One-click agent connect. Writes the MCP config for whichever client you use, keeping any servers already in the file.
- A schema tree in the Explorer. A lock for private tables, an open lock for public ones, and per-user tables name the column that scopes them. Click a model to jump to it.
- A status bar shield with the critical count, and autocomplete plus validation on schema.cresco.
Settings: crescodb.auditOnSave (on by default), crescodb.cliPath if cresco is not on your PATH, and crescodb.studioUrl if the dev server is not on the port in your config.json. If the audit cannot run, the status bar says so rather than looking clean, and CrescoDB: Show logs says why.
Assistant & coding agent
The dashboard's AI page has two tabs. Chat designs schema and data around your actual project, describe what you're building, review the proposal, hit Apply to DB. Code is the coding agent: it reads and writes your project's files and runs commands (each command asks for your approval first; file edits show a diff with undo). Both need cresco dev running, the AI works on your real project.
- Conversations: the sidebar keeps your history like ChatGPT: chats on the Chat tab, builds on the Code tab. Start new ones freely; old ones stay until you delete them. History lives in the project's own database, per project.
- Attachments: the clip button sends images and code/text files with your message (screenshots of a schema, a CSV to model, an error log).
- Edit & stop: edit a sent message to rewind the chat and resend; the send button becomes Stop while the AI works.
- Settings tab: per-project preferences saved to config.json: reply tone, project memory (whether the AI sees your schema and sample data), review-every-file-edit mode, and auto-run commands for trusted projects.
The assistant and agent run on CrescoDB's hosted keys and are metered in Cresco tokens on your account, so you never bring a provider key. cresco login connects the CLI to your account.
Scaffolds with cresco create
Writes a feature into your project as full, editable code: real files, not a package. Models merge into your schema, endpoints go live on cresco dev immediately, and every file is yours to change.
- auth: register / login / me, JWT + scrypt, middleware for your own routes
- blog: posts + comments, a slugged public feed and comment API, plus a ready-made blog website (home + post pages) served at /app
- payments: Paystack + Flutterwave checkout & verify, recorded in a payments table. No keys yet? It runs in dev mode (instant fake success) so you can build the flow first.
- store: products, cart and checkout with a storefront UI; the total is computed on the server (never the client) and composes with the payments feature automatically.
- chat: a sign-in-gated, Slack/Discord-style realtime chat: servers, channels and live messages over SSE, with a full chat UI. Bring your own email provider to verify sign-ups.
- todo: a complete full-stack to-do app (list UI + API) that syncs live across tabs, the fastest end-to-end starter.
Modules
cresco add installs managed, database-aware modules, they merge models into your schema, drop in code, and record a migration. Use create when you want to own the code; use add for managed installs and community modules from the registry.
File storage
Every project has bucket-based file storage at /storage: uploads, folders, public or private buckets, and a file browser in the dashboard. Locally files live in .cresco/storage; in production point config.storage at any S3-compatible store (AWS S3, Cloudflare R2).
Public buckets serve files at a plain URL; private buckets require auth. Toggle visibility from the dashboard's Storage page.
Realtime
Subscribe to live row changes, every create, update and delete streams to connected clients over SSE, with table access rules and per-row security respected. The dashboard's Realtime page shows the live feed.
The JS SDK, cresco-js
A zero-dependency client for browsers and Node 18+: auth, fluent CRUD, storage, and realtime in one import.
Production with cresco start
The production server serves only your API, auth, storage, realtime and custom routes, every admin surface (SQL editor, schema DDL, AI, dashboard) is absent, not just disabled. Configure with env vars: DATABASE_URL, PORT, CRESCO_AUTH_SECRET, CRESCO_CORS_ORIGIN; pending migrations run at boot. Server-to-server callers can use the project API key for full access.
Access rules always apply in production, with or without auth. If CRESCO_AUTH_SECRET is not set, nobody can sign in, so every caller is treated as a guest: only tables your schema marks "read": ["*"] are readable, nothing is writable, and tables scoped to an owner are closed. The project API key still has full access, and the startup banner lists exactly which tables are public. (Before October 2026 an app started without the secret had no access control at all; if yours relied on that, mark its public tables "read": ["*"], or set up auth.)
Login on the built-in auth is limited to 20 failed attempts per email and 100 per network address every 15 minutes. Behind a proxy, only proxies on a private network are trusted for the client's address; set CRESCO_TRUST_PROXY (true, a hop count, or a list of ranges) if yours is elsewhere. For Postgres over TLS, use sslmode=verify-full where your provider supports it: sslmode=require encrypts but does not check the server's certificate.
Deploy to a server you own
CrescoDB Cloud is a deployment control plane: it builds and runs your project on infrastructure you own, and you manage it from the dashboard's Cloud area. Your app and its database live on your server or provider. CrescoDB orchestrates the deployment; it doesn't host your data. Three ways to deploy:
- Your own server (BYO-VPS). Connect any Ubuntu/Debian VPS with a one-line installer, it creates a locked-down deploy user and installs Docker. Then cresco deploy --server <name> ships your local project, or deploy a repo from the dashboard.
- Git auto-deploy. In Cloud → Deploy a repo, install the CrescoDB GitHub app on the repos you pick, choose a repo + branch, and deploy. Every push to that branch redeploys automatically; toggle auto-deploy per app.
- Managed PaaS (one-click). Connect your Render or Railway account (an API key, stored encrypted) and deploy a repo there in one step. The provider builds, hosts, and auto-deploys on push; the dashboard links out to it for logs and env. This route doesn't need the GitHub app: the provider clones the repo itself, so you can just type owner/repository. (A private repo needs GitHub connected inside your provider account.)
Each deployed app has its own Overview, Deployments, Environment, Domains, Logs, Database, and Settings. Environment variables and secrets are encrypted at rest; custom domains get automatic HTTPS via Caddy on a BYO-VPS. You can also edit a live deployment's data and schema from the dashboard, open a live app and its Tables / SQL editor operate on the running database.
Live progress, not polling. Deploy status, build logs, and a server's first check-in stream to the dashboard as they happen, so you watch a build move through queued → building → deploying → live in real time.
Connecting your first server, start to finish
If you have never rented a server before, this is the whole process. It takes about five minutes and none of it requires knowing Linux.
- Rent a box. Hetzner, DigitalOcean, Vultr and Linode all rent one for roughly $5 a month. Choose Ubuntu (any recent version) and the smallest size. You will be shown an IP address and either a password or an SSH key. That IP is your server.
- Add it in the dashboard. Cloud → Servers → Connect a server. Give it a name you will recognise later, like web-1. We generate a key pair for it and show you one command.
- Paste that command into your server. Log in to the box (your provider has a "Console" button in the browser if you would rather not use a terminal) and paste. It looks like this:
$ curl -fsSL .../connect.sh | sudo bash -s -- --token ... --url ... --pubkey '...' # the dashboard fills all three in for you, copy the whole lineIt installs Docker if the box does not have it, creates a dedicated non-root cresco user with no sudo access (it is added to the docker group so it can manage containers), authorises our key for that one user, and reports back. We never ask for and never store your root password. The only credential we hold is a key that logs in as cresco, and it is encrypted at rest on our side.
- Watch it turn green. The server appears as online in the dashboard within a few seconds, along with its OS, CPU count and memory.
- Deploy. From your project folder: cresco deploy --server web-1. Your project is sent up, built on the box, started and health-checked. If the build fails, the previous version keeps serving. If the new version builds but fails its health check, you are told why, and Roll back puts the previous build back in seconds.
Deploy a zip
No git and no CLI needed: Cloud → New deployment → Upload a zip, pick the server, and drop a .zip of the project (GitHub's "Download ZIP" works as it is). We run every SSH command. To ship a new version, open the app and use Deploy an update.
- Unpacked safely. A path that leaves the project folder, an absolute path, a symlink, a password-protected zip or one that expands far beyond its size is refused. node_modules, .git, .env files and local databases are left out; the server installs dependencies itself. Up to 100 MB zipped.
- Checked before anything reaches your server. A CrescoDB project gets the same access audit as cresco deploy: a table of personal data readable by anyone, or an API with no sign-in, stops the deploy. Every project is scanned for secrets written into the code (private keys, live Stripe and AWS keys, GitHub, Slack and AI provider tokens, database URLs with a password). Findings name the file and line, never the value. You can deploy anyway when it is intended, and the build log says you did.
- Built like the CLI. Your Dockerfile is used if there is one, so any language works. Otherwise a CrescoDB project, or a Node app with a start script, gets the Dockerfile cresco deploy writes. Anything else is refused with the reason, before the build.
- Redeploy rebuilds from the same upload for 7 days; after that, upload the zip again.
An app that deploys from GitHub does not take zips: the next push would replace it. Zip deploys go to your own server; Render and Railway deploy from a repo. Zip deploy and its checks are on every plan; the apps you deploy count against your plan's app limit as usual.
Partitions: dividing a server's disk between apps
A partition allocates a fixed amount of your server's disk to separate app spaces. A 100 GB server can be split into, say, 40, 30 and 20 GB, and each app deployed into one of them. When an app fills its partition it gets "no space left", and the other apps and the server itself are unaffected. Manage them from Cloud → Servers → Partitions; choose one when you first deploy an app.
- The space is really set aside on the disk when the partition is made, not promised. Each is a fixed-size filesystem on the server, kept out of the weekly disk trim, and checked on every read so nothing can quietly take its space back.
- An app in a partition can only write inside it. Its database, uploads and code live there, and the rest of its container is read-only, apart from a 256 MB in-memory /tmp. Docker images and container logs stay on the main disk (logs are capped by your plan).
- Limits. At least 2 GB each. 10% of the disk, and never less than 5 GB, is always kept free for the system. A partition can grow at any time without downtime; it cannot shrink. One that still holds data cannot be removed.
- Enabling it needs root once. You run one command as root that installs a small program, /usr/local/sbin/cresco-parts, and allows our deploy user to run that program and nothing else. The dashboard shows its source before you run it, and one command removes it.
- Plans. Pro: 3 per server. Team: 10. Enterprise: up to 20. Not on Free.
What a partition is not. It allocates disk space only. It does not limit CPU, memory or network traffic (set CPU and memory limits per app in its settings), and apps on the same server are separate containers, not separate machines, so a partition is not a security boundary between them. (Separately, every app and its database run on a Docker network of their own, so one app cannot reach another app's database. Apps deployed before 9 October 2026 move to their own network on their next deploy.) An app that runs from / (no WORKDIR in its Dockerfile) cannot run from a partition.
The connect command carries a single-use token that stops working the moment the server checks in, so a copy of it left in your shell history is worthless. If a connect goes wrong, re-issue a fresh command from the server's page; the old one dies instantly.
Plan limits. Two things count against your plan: connected servers and apps deployed to them.
- Free: 1 server, 1 app
- Pro: 5 servers, 15 apps
- Team: 20 servers, 75 apps
- Enterprise: unlimited
Redeploying an app you already have is always allowed, so a cap can never block a fix to something already live. Apps you deploy to a managed PaaS are not capped, because the provider hosts those, so we don't meter them. Local development is not capped by anything, ever.
Leaving, with cresco eject
Every platform says it has no lock-in. Here is ours, as a command you can run right now before you have committed anything to us.
cresco eject
That writes a folder next to your project that runs it without CrescoDB. No account, no connection to our servers, and no dependency on us continuing to publish anything:
myapp-ejected/
schema.cresco your models
data.sql every table and every row, as plain SQL
vendor/crescodb-1.0.3.tgz the engine itself, packaged into the folder
Dockerfile builds from that file, not from the internet
docker-compose.yml your app and a real database, wired together
README.md
…the rest of your project, as it was
Then:
cd myapp-ejected
docker compose up --build
Your API is on http://localhost:3000. That is the whole procedure.
What this actually guarantees, and what it does not
Being precise here matters more than sounding good, because this is the promise people check.
- It is not an airgapped bundle. docker compose up still downloads a Node base image and still lets npm fetch ordinary third-party packages, the same as any Docker build anywhere.
- What it removes is the dependency on us. The engine is inside the folder as a packaged file, so your app keeps building even if CrescoDB is gone from npm, this CLI is uninstalled, and the company no longer exists. Everything else it needs belongs to somebody else and does not care whether we do.
- And underneath that, your data is plain SQL. data.sql is not a proprietary snapshot only we can read. psql, mysql and sqlite3 all load it, and so does anything else that speaks SQL. If you decide to stop using CrescoDB altogether, your data walks out with you and you need neither our permission nor our software to read it.
Export for a different engine with --db. This is also the clean way to move a SQLite project onto Postgres, because the schema is translated properly rather than dumped in the dialect it happened to start in:
cresco eject --db postgres
If the data export fails for any reason, eject writes nothing at all and tells you why. A bundle whose data.sql is missing or empty fails the way an untested backup does: you find out on the day you needed it. Same reasoning as the restore checks.
Two things to change before you put a bundle anywhere real, both called out in its README: the database password in docker-compose.yml is a placeholder that is identical in every ejected bundle, and CRESCO_AUTH_SECRET has to be set if your project uses auth.
The dashboard has the data half of this too, at Settings → Connection → Export, including exporting for a different engine. cresco eject is that plus everything needed to actually run.
Custom domains & DNS
This is the step that stops most people, so it is worth being plain about what is actually happening. A domain is just a name that points at a number. Your server has a number (its IP address). To put your app on app.yoursite.com, you tell your domain provider "this name lives at that number". That is the whole job.
Open your app in the dashboard, go to Domains, and type the domain you want. We then show you the exact record to create, filled in with your server's address:
Type A
Name app (just the part before your domain, not the whole thing)
Value 203.0.113.40 (your server's IP, filled in for you)
TTL Auto (or 300)
Proxy DNS only
Paste that into whichever provider holds your domain. Cloudflare, Namecheap, GoDaddy, Porkbun, they all have the same five fields under a heading like "DNS" or "Manage DNS". The dashboard has step-by-step directions for the common ones behind the ? next to the record.
Two things people get wrong constantly, so we check both for you:
- The Name field is not the whole domain. For app.yoursite.com you type app, not app.yoursite.com. Providers add the rest. For the bare domain, type @.
- Cloudflare's orange cloud must be off. If the record is set to "Proxied" (the orange cloud) instead of "DNS only" (grey), Cloudflare answers on your behalf, your server never sees the certificate check, and you get a 522 error. We detect this specific case and say so by name, instead of leaving you staring at a timeout.
Press Check DNS and we look up the name from the outside and tell you one of: nothing resolves yet, it points somewhere else (with where), it is proxied through Cloudflare, or it is pointing at your server and certificates can issue. New records usually work within a few minutes.
Once it resolves, HTTPS is automatic. Caddy requests a free Let's Encrypt certificate and renews it forever. You do nothing.
Keeping Cloudflare's proxy on (DNS-01)
Some people want the orange cloud: it hides your server's IP and absorbs traffic. You can have both, but the certificate then has to be verified a different way, through your DNS records instead of through your website. That is called a DNS-01 challenge, and it needs permission to write a record on your behalf.
In Domains → Advanced, pick Cloudflare and paste an API token. Create it at Cloudflare → My Profile → API Tokens → Create Token, using the "Edit zone DNS" template scoped to that domain's zone. A Global API Key is not the same thing and will not work.
We then swap in a Caddy build that speaks Cloudflare, store your token on your server with 0600 permissions, and validate the whole configuration before replacing the running one. If any step fails, the previous working setup is put back and Caddy keeps serving. Your token is never written to a log.
Cloudflare is the only DNS-01 provider today. Everyone else should leave the record on "DNS only", which is the default and needs no token at all.
Server & app health
Open a server in Cloud and you get live numbers read off the box itself, not estimates:
- CPU: real utilisation, sampled twice a second apart. This is not load average, which is a different number that confuses everyone. Time spent waiting on the disk is counted as idle, because the processor is.
- Memory: used, available and total. "Available" is the honest figure: Linux uses spare memory for disk cache and gives it back on demand, so a box that looks 90% full is usually fine.
- Disk: used and free on the root filesystem, plus what Docker itself is holding in images, containers and volumes. A full disk is the single most common way a small VPS dies, and it is almost always old Docker images.
- Uptime, load, kernel, OS, Docker version, and how many containers are running versus stopped.
Each app has its own panel: its container's CPU and memory, its restart count, its published port, and its database container's size on disk.
Restart count is the one to watch. A container that crashes and immediately restarts reports itself as "running" to every tool that asks, because it just started. It will do that forever while serving nothing. We report the honest status and the restart count, and alert you when it stops.
Languages
Your app can be written in any language: it just talks to the REST API over HTTP (the CLI needs Node installed to run, like Docker for a database). Scaffold code is currently provided for:
- Node: available now (auth, blog, payments, store, chat, todo), plus Express and Next.js variants
- Python, Go, PHP are on the way
Accounts & sign-in
A CrescoDB account unlocks the hosted features, the AI coding agent, cloud backups, publishing, and shared projects. The dashboard requires a signed-in account; the CLI and API work offline once you're set up.
Sign up with an email and password, or with GitHub or Google. Accounts start on the free plan; you can upgrade from Settings at any time.
cresco login uses a device-authorization flow: it shows a short code, opens crescodb.com/activate, and once you approve it (while signed in) the CLI is connected. The token is stored in ~/.cresco/config.json. Paid features are checked against your plan on the server.
Two-factor authentication
Turn it on in Settings → Security. It uses TOTP, so it works with 1Password, Bitwarden, Google Authenticator, Aegis, or anything else that scans a standard setup key. No SMS, and no emailed codes.
Emailed codes are the common choice and they're weaker than they look: they protect against a leaked password but not against the attack that actually happens, which is the mailbox itself being taken over. If the password-reset link and the second factor both land in the same inbox, the second factor is a rename of the first. TOTP also works with no signal.
- Setup is two steps: we hand you a key, and 2FA only switches on after you enter a working code. Abandon it halfway and nothing has changed, you can't lock yourself out.
- You get 10 single-use recovery codes, shown once. Store them somewhere that is not the phone running your authenticator, because that phone is what they exist for.
- Turning 2FA on or off, and regenerating recovery codes, all require your password again. A stolen browser session can't enrol its own authenticator or strip the protection off.
Passwords need at least 10 characters and are checked against known breach corpora (only a 5-character hash prefix ever leaves the server, your password is never sent anywhere). There are no "must contain a symbol" rules: length is what survives an offline crack, and composition rules mostly produce Password1! reused on ten sites. A long passphrase is accepted; a short one full of symbols is not.
Backups, restore checks & rollback
Every host tells you "backup succeeded". That only ever means a file was written: not that it's complete, not that it's valid, not that it can be loaded back. The gap between those is where people lose their data, and it stays invisible until the day it matters.
So we restore it and look.
- Restore checks take your latest backup, load it into a throwaway database container on your server, count what came out, then destroy the container. Your live database is never touched. The app page reports the result: Restored into a clean database: 12 tables, 48,120 rows. Took 4.2s.
- A backup that loads cleanly but contains nothing is reported as a failure, not a pass. An empty file is not a backup.
- Rehearsals run automatically about once a week per app, and you can run one any time from Overview → Safety net → Test now. They're paced deliberately: a real restore costs CPU and disk on your server, and a check that slows your box is one you'd switch off.
- Rollback checks confirm the previous image is still on the server and still boots, before you need it. They also warn when migrations have run since that image, because rolling back code without rolling back the database is its own outage.
Until a backup has actually been rehearsed, the dashboard says "Never tested" rather than showing a green tick. That state is deliberate, treating "no news" as good news is the whole problem.
Production monitoring
Your API runs on the CrescoDB engine, so it can watch itself from the inside. There is no SDK to install and nothing to add per language: the engine already sees every request and every query, so it only has to report them. Open it in the dashboard under Monitor → Monitoring.
What the dashboard shows for each app:
- Every route, as its template (GET /posts/:id, not the raw path): requests, error rate, server and client errors, p95 latency and the slowest single request.
- The slowest queries, grouped by shape: SELECT * FROM Order WHERE userId = ?, with how often each ran, the average and slowest time, and how many failed.
- What is failing now, named by route, so "checkout is broken" shows up as POST /checkout while the rest of the app is fine.
- Errors, one row per bug, with the stack pointing at the line in your code. See below.
- Runtime: event loop delay and memory, minute by minute.
Errors
Every way a request or the process can fail is caught, with nothing added to your code:
- An exception in a route, sync or async, in your own routes or the generated API. The caller gets {"error":"Internal server error"} as JSON, never the stack; the stack goes to your log and to the dashboard.
- A database error, with its code and the statement's shape, even when your code catches it. A duplicate email or a missing field is the caller's mistake (a 4xx) and is not counted.
- A request that hung: the visitor waited 10 seconds and gave up.
- A 5xx your code sent on purpose, and an error you caught and passed to console.error (listed, never alerted; set CRESCO_MONITOR_CONSOLE=0 to turn this off).
- An unhandled promise rejection and a crash. The process still exits with code 1, exactly as Node would, after the report goes out. If it cannot get out, it is written to disk and the next process sends it. An app with its own uncaughtException handler is never exited by ours.
By plan: every plan catches errors. Free shows the last 24 hours and keeps up to 25 distinct errors per app; Pro 30 days and 500; Team 90 days and 1,000; Enterprise 90 days with no cap. Errors that age out of view stop counting towards the limit, and upgrading shows the older ones again. Error alerts count against your monthly alert allowance like any other.
Errors are grouped by where they happen in your code (function and file, not the line, so a deploy that moves a line is the same bug), never by the message. Resolve one when it is fixed: if it happens again it reopens and you are told. Ignore one to keep counting it without alerts. Messages lose their values before they leave the app: emails, numbers, IDs and quoted input become ?, and stacks carry paths relative to your project.
Turning it on
Monitoring runs in cresco start, the production server, and only when it has a monitoring token. No token, nothing leaves the machine.
- Deployed with CrescoDB Cloud: nothing to do. The deploy creates the token and sets it on the container.
- Hosted anywhere else (Railway, Render, Fly, your own box): Monitor → Monitoring → Add an app, then copy the two variables it shows into that host's environment and restart.
The startup banner says monitoring on when it is wired. The token is shown once; if you lose it, rotate it from the app's menu and the old one stops working.
What is sent, and what never is
- A summary once a minute, not a stream: per-route counts, latency percentiles and the slowest query shapes. It is the same size at 1 request a minute and at 10,000.
- Route templates, never raw paths. Ids in a path become :id. Anything that matched no route is counted in one (unmatched) bucket, so a scanner walking /wp-admin cannot fill your dashboard.
- No values. No request bodies, query strings, headers or SQL parameters. Query text is reduced to its shape, with every literal replaced by ?.
- It never slows a request. Recording is a few counters when a response finishes. Sending happens in the background with a short timeout, so if we are unreachable you lose a minute of data, not a request.
Each summary also counts as a heartbeat. An app that stops sending for three minutes raises stopped reporting, which for an app on Railway or Render is the only down alert there is, because nothing else of ours can see it.
Your own alert rules
Built-in monitoring alerts when a route is mostly failing. For the lines only you know, add a rule to the app under Your alert rules: a route or the whole app, one of p95 latency, server errors, error rate or requests, above or below a number, over 5 to 60 minutes. "POST /checkout p95 above 800 ms over 10 minutes." "The whole app, requests below 10 over 60 minutes", for traffic that quietly stops.
- Rules are judged on every report (once a minute) and alert once, when the line is crossed. They clear 10% back past the line, so a value sitting on it cannot flap.
- p95 and error rate need at least 10 requests in the window, so a quiet hour cannot set them off. A "below" rule waits until the app has reported for most of the window: an app that is down is the job of "stopped reporting".
- Free includes 3 rules, Pro 25, Team 100, Enterprise unlimited.
Server monitoring
Every server connected to CrescoDB Cloud is read every five minutes, over the same locked-down SSH user the deploys use. Nothing extra is installed. Open one under Monitor → Monitoring, in the Servers list.
- CPU, with load average and core count, charted over 6 hours, 24 hours or 7 days.
- Memory against what is available, not against what is "free". Linux lends spare memory to the disk cache and takes it back on demand, so "available" is the honest number. Swap is shown beside it.
- Every partition, fullest first, with size, used, free and inodes. Temporary and image filesystems (tmpfs, overlay, snap) are left out, because they are never what fills up. When a partition is on course to fill within a week at its recent rate, the page says when.
- Every certificate your apps' domains are served with, and the days left on each, read from the proxy on the server itself.
- Every container, with its status and its memory against its limit.
- Things that need you: pending security updates, a reboot the system is waiting for, and system services that have failed.
Readings are kept for 7 days. If the last reading is more than 15 minutes old, the page says so instead of presenting old numbers as current.
Your own numbers, per server
Each server has its own Alerts for this server settings: when disk, inodes, memory, CPU, swap, a disk-fill forecast, a certificate, waiting updates or a waiting restart should alert, and which alerts to send for it at all. A database box can warn at 80% disk while a web box stays at 90%. The clear point follows each number at the same distance as the defaults, so no setting can make an alert flap.
Fixing it from the alert
Alerts link to the server with the fix ready. Nothing runs from the link: it opens a dialog that says what will happen, and only its button acts.
- Clean up is always available. It previews what can be reclaimed first, then removes leftover Docker layers, build cache and stopped containers. It never touches volumes, where databases live, and never the current or previous image of an app, so every rollback still works.
- Restart Caddy, Start Docker and Restart server need root, and our connection to your server is deliberately not root. They work only after you allow them, once, by running the command shown on the server page as root. It allows exactly those three commands for the deploy user and nothing else, checks the file with visudo before keeping it, and is undone with sudo rm -f /etc/sudoers.d/cresco-fixes /etc/cresco-fixes. Restarting the server asks you to type its name.
- Everything else, such as a failed service or waiting updates, gets the exact command to run. Which service to restart, and when to install updates, stays your call.
Uptime checks
Everything else in Monitoring is reported from inside your app or your server. An uptime check fetches a public URL from the CrescoDB platform, the way a visitor would, so it sees what nothing inside can: a DNS record pointing at the wrong place, a CDN serving an error page, a certificate a browser refuses, a page that quietly changed. Add one under Monitor → Monitoring → Check a URL; it works for any public URL, wherever it is hosted.
- Every check records DNS time, connect, TLS, time to first byte and the total, the status, and the certificate. The page shows where the last response's time went, uptime over 24 hours and 7 days, and every result for 7 days.
- Down means two checks in a row failed: no answer, a DNS or certificate error, an unexpected status, or text you said must be on the page missing. One failure is a blip.
- Slower than a limit you set, twice in a row, alerts too. So does a certificate with under 14 days left.
- Watch the content keeps a fingerprint of the page (never the page) and tells you once when it changes from the version you accepted: a wrong deploy or a defacement. Pages that change on every load are better watched with "must contain" text.
- Only public addresses are checked, on ports 80, 443, 8080 and 8443. A name that resolves to a private address is refused, and every redirect is checked the same way.
- Free includes 3 checks every 5 minutes; Pro 25, Team 100 and Enterprise unlimited, every minute.
Alerts
We tell you when it breaks, and every alert names what broke: the route, the app, the server or the partition. Alerts reach you by email on every plan, and by Slack or Telegram on paid plans. Your account email is connected automatically; Pro can add 2 more addresses and Team 5, each confirmed by a link before it receives anything.
- A route is failing: at least 3 server errors that are at least half of that route's requests, over five minutes. It clears under 20%. A quiet route does not count as recovered.
- App stopped reporting: no summary for three minutes.
- New error: a bug we have not seen in this app before, with the error and the line. At most 3 per report and 10 an hour per app; the rest wait on the Errors list.
- Error came back: one you marked resolved happened again.
- App crashed: the process exited on an uncaught error.
- Event loop blocked: requests waited half a second or more behind synchronous work, for three minutes. Clears under 200 ms.
- Server unreachable: two reads in a row fail, about ten minutes.
- Disk almost full: any real partition at 90%, named in the alert. Clears under 85%.
- Memory pressure: 95% in use for about ten minutes. CPU saturated: 95% for about fifteen. Heavy swap: 80% of swap in use for about ten.
- Caddy down or Docker stopped for about ten minutes, with the command that starts it again. Caddy is only watched on servers where we have seen it running, so a box that uses Traefik is never paged for it.
- Server restarted, saying whether a restart had been waiting for updates (probably planned) or not (it crashed, or your provider restarted it).
- A system service failed, by name, with the command that shows why.
- Security updates waiting a week, and a restart waiting 3 days to finish installing them.
- Out of inodes: a partition that has used 90% of the files it can hold, which stops writes even with space left.
- Disk filling fast: at the rate it grew over the last 6 hours, a partition will be full within a day.
- Certificate expiring: the certificate your proxy is actually serving for an app's domain has under 14 days left, which means renewal has been failing.
- Container stopped, with the exit code, and not responding: running, but the health check fails. That is the one that looks fine in docker ps.
- Deploy failed, with a note that your previous version is still serving traffic, and backup problems: a backup that did not complete, or a restore rehearsal that failed.
An alert is not sent until it arrives. If Slack, Telegram or the email provider fails to take it (a timeout, rate limiting, an outage), we try again after 30 seconds, 2 minutes, 10, 30 and 120, and every attempt survives a restart on our side. A channel that refuses for good, such as a deleted Slack webhook or a blocked bot, is not retried: the delivery log says so at once, so you can fix it. Retries are never charged again.
You get one alert per problem, not one per check. An app that has been down for six hours sends a single alert, not seventy-two. Recovery is shown in the dashboard and not sent at 4am. If we lose touch with most of your servers at once, we hold the alerts, because that is far more likely to be our side than all of yours.
Every plan includes alerts: Free 50 a month by email, Pro 500, Team 1,200, Enterprise unlimited. An alert costs one per channel it reaches, however many addresses that channel has. When the month's allowance runs out you get one notice on every channel, which is not counted, and a note in the dashboard's notifications. Under Monitor → Alerts you choose which alerts you receive, see exactly when each one fires and clears, and read every delivery with what each channel answered.
Incidents & silences
An alert is a message. An incident is the problem it was about, and Monitor → Incidents lists every one: what is broken now, since when, who is on it, and the full timeline of each, from the first alert to resolved.
- It opens the first time an alert fires for a problem. More alerts about the same problem are added to it, not opened as new incidents.
- It closes itself when the problem clears: the route recovers, the server answers, the disk drops back under its line. If the same thing breaks again within an hour, the same incident reopens, so a problem that keeps coming back is one incident with a long timeline.
- One-off events (a failed deploy, a backup that would not restore, a server restart) close after a quiet period, or as soon as the next deploy or restore succeeds.
- Acknowledge, investigating, resolve, notes: for the people working it. Every step lands on the timeline.
Silences hold the messages for planned work, on everything, one server (and the apps on it) or one app, for as long as you set. The incident is still recorded, marked silenced, and nothing held is charged. A deploy silences its own app's health alerts while it runs and five minutes after, and a restart from the dashboard silences its server for fifteen minutes. A failed deploy is never silenced.
Recovery messages are off by default, so nothing arrives at 4am to say that something you never saw is fine. Turn on Message me when it recovers and a "Resolved" message goes to the same channels as the alert, counting like an alert.
Publishing modules
Anyone on a paid plan can publish modules to the registry. Describe the module with a cresco.module.json in its folder, then run cresco publish:
Install any published module with cresco add <name>: it merges the models and drops the code, just like a bundled module. Public modules install for anyone; private (scoped @org/name) and paid modules are gated to entitled accounts.
Modules are just files: frontend code publishes like backend code. Ship a UI kit, a set of React components, or a full login page as a module: put the files in the module folder, publish, and cresco add copies them into whichever project needs them (it never overwrites files you already have). Models are optional, a pure-frontend module has none.
Teams
An org (team) is a shared workspace with its own private module scope. Create one and the slug becomes your @scope; members can install the org's private modules.
Publish a private module to the org with a scoped name (@acme/billing), only admins/owners can publish, and any member can cresco add @acme/billing. A private registry requires a Team plan.
Shared live projects
Beyond modules, a whole project can be shared so teammates anywhere work against the same live database. Point the project at a shared Postgres/MySQL (a Railway/Neon/Supabase or your own server. Settings → Connection), then:
Anyone in the org (signed in to their own CrescoDB account, anywhere in the world) can cresco open it; access is checked server-side on every open. The shared connection lives in the project's config, local SQLite/embedded projects share their schema but not their data, so use a shared server for a true shared-live project. Re-running cresco link syncs the latest schema.
Plans & billing
Local development is free forever. Plans differ by capability, not just usage limits. The short version: everything on your own machine is free, and you pay for the things that run while you are asleep.
- Free: everything local plus AI chat, and 1 app deployed to 1 server, with 50 alerts a month by email and 3 alert rules. No backup safety net: no scheduled backups and no restore rehearsals. That is deliberate and the dashboard says so plainly rather than hiding it.
- Pro: the AI coding agent with smart model routing, nightly backups with restore rehearsals (25 stored, 7 days), alerts by email, Slack or Telegram (500/month, 2 extra alert addresses, 25 alert rules), 15 apps across 5 servers, 10 shared projects, registry publishing, 48-hour email support.
- Team (per seat), everything in Pro plus team workspaces, shared live projects, a full audit trail, the private @org registry, 100 backups at 30 days, 1,200 alerts a month with 5 extra alert addresses and 100 alert rules, 75 apps across 20 servers, 50 shared projects, 12-hour priority support. (Pro users can join a team; creating one needs Team.)
- Enterprise: our most capable model first on every big task, unlimited servers, apps, projects and stored backups, 90 days of retention, uncapped alerts, a named contact with a 4-hour response.
Two different things are counted and it is easy to mix them up: apps are deployments running on a server you own, while projects are projects registered to share with a team via cresco link. Deploying does not consume a project, and sharing does not consume an app.
Assistant and agent usage is measured in Cresco tokens. Every plan includes a monthly amount (see pricing); each request deducts tokens based on its size, small requests use a few, large agent tasks use more. Usage also has daily and weekly ceilings within the month. Model selection is automatic. Your token balance is shown in the dashboard.
Billing is managed from dashboard → Settings → Billing. Prices are shown in USD; you pick your currency at checkout.