← All guides

2026-10-11

Create Your Own Neon

The many database pattern is an architecture in which uses many single-tenent, potentially ephemeral databases to track state for small applications or agents. We see this pattern commonly in agent harness design in which each agent gets its own database, or even a database per project or thread, in data pipelines where one may want to create a fresh database for every run, and in CI systems where you want to provide a production-like test state. Virtual machines, especially micro-vms powered by Firecracker, work great in this pattern due to their density potential and fast creation times; combined with object store backing, this also provides a cheap cold storage mechanism on top of the isolation inherent in the pattern.

This guide will walk you (or your agent) through the mechanics of running hundreds of VMs on top of the Heyo control plane.

hint: replace the ".html" with ".txt" in the browser and you get an all text version to provide to your coding agent

What you will need:

  • A Heyo account and a namespace, sign-up here
  • An auth token scoped to that namespace, launch the dashboard after creating a namespace to make one
  • heyctl or use either the Rust or TypeScript SDKs

Plugins

Heyo Computer provides a control plane for managing VMs, networks, certs, and security that we call the app-lb. On top of the app-lb we also provide plugins to manage additional feature sets like authorization, CI, and observability.

Prefer the console? Everything in this section can be done from your namespace's dashboard on the app-lb (Heyo opens it after you sign in): press Plugins in the top bar, use a plugin's Install button, and Open dashboard on the ones that have a page. The rest of the guide uses the CLI and the SDK, because that is how your application will do it.

In this guide we'll use the following:

Plugin Name In your namespace What?
postgres Postgres install it A dedicated database per name, each its own microVM with its own role and password; the credentials are also written to the namespace secret pg-<name>.
secrets Secrets auto-installed A page for the namespace's secrets: the API keys and credentials deployments read with env_from. Key names only, never values.
obs Observability auto-installed Logs, metrics and error alerts for every VM in the namespace.

If you are using the heyctl CLI:

export HEYO_TOKEN=applb_…          # from the dashboard
heyctl login --server https://admin.us5.heyo.work --token "$HEYO_TOKEN"
heyctl plugins install postgres -n acme
heyctl plugins list -n acme      # each should show `installed`

Or via SDK (@heyocomputer/hws on npm, hws on crates.io):

import { Hws } from "@heyocomputer/hws";

const lb = new Hws({ server: "https://admin.us5.heyo.work", token: process.env.HEYO_TOKEN });
await lb.installPlugin("acme", "postgres");

A call to a plugin that is not installed fails with a ConflictError whose code is plugin_not_installed, and whose message names the install command. Running an app-lb on your own hardware instead? The operator's half is in Plugins.

Create Databases from Code

Heyo abstacts the heavy lifting away from running VMs by maintaining a Postgres 18 VM image behind a proxy to manage connections; the postgres plugin provides databases with it its own microVM, role and password. This lets you spin up databases on demand and control their lifecycle:

import { Hws } from "@heyocomputer/hws";

const lb = new Hws({ server: "https://admin.us5.heyo.work", token: process.env.HEYO_TOKEN });
const pg = lb.postgres("acme");

const db = await pg.createDatabase({ name: "main" });
console.log(db.database, db.status);   // ns_acme_main  provisioning
// db.url is postgres://ns_acme_main:…@<pooler>:6432/ns_acme_main?sslmode=require

The credential works as soon as the call returns. pg-fc boots the database's VM in the background, and the first connection waits for it. Names are 1–31 lowercase letters, digits or _, starting with a letter; on the pooler each becomes ns_<namespace>_<name>, so two namespaces never collide.

The password comes back this once. It is also written to the namespace secret pg-<name> (url, host, port, database, user, password), so nothing has to copy it around: a deployment reads it with env_from, and a later process reads the secret rather than the original response. Pass secret: false when you will store the credential yourself.

const all = await pg.databases();     // names, physical databases, live or not, never a password
for (const d of all.databases) console.log(d.name, d.database, d.live, d.secret);

await pg.deleteDatabase("main");      // revokes the credential

Deleting revokes the credential at once. The data stays on the pool until it is purged, and the pg-<name> secret stays until you delete it, so a deletion by mistake is recoverable. Each namespace may hold a limited number of databases (10 by default). The same calls exist in Rust, as lb.postgres(ns).create_database(&NewDatabase::new("main")) in hws.

Running your own app-lb rather than the managed one? Then the pooler and its image are yours to set up once: see Build Your Own Serverless Postgres, then enable the plugin as in Plugins.

Scale to Zero

You don't configure scale-to-zero per database: every database will scale down according to the TTL you set (set to zero to keep it alive forever). The pooler stops a database's VM after a stretch with no connections, and the next connection brings it back. The TTLs that decide when:

Setting Default What it does
Idle timeout 15 minutes Stop a database's VM after this long with no client connections. Jittered by ±15% so a fleet of databases does not stop at once.
Fast idle timeout 60 seconds A shorter timeout for databases whose last start was quick, since bringing them back costs little.
Cold tiers by idle time A database stopped for longer is compacted, then frozen, then moved to cheaper storage, each step making its next start a little slower.

A stopped database costs a cold start on the next connection: about a second for a recently stopped VM, depending on the size of the database.

Connecting Your Application

Every database has a standard Postgres connection string, postgres://<user>:<password>@<host>:6432/<database>?sslmode=require, and three places to get it.

From the SDK, when you create it. createDatabase returns the connection string as url, along with each part on its own:

const db = await lb.postgres("acme").createDatabase({ name: "app" });
db.url;                                   // the whole connection string
db.host; db.port; db.database; db.user; db.password; db.sslmode;

This is the only time the password is returned, so use it or store it now. In Rust, create_database returns the same fields on CreatedPostgresDatabase (whose Debug output leaves the password and URL out, so it is safe to log).

From the console. In your namespace's dashboard, press Plugins, then Open dashboard on Postgres. Create a database there and the page shows its connection string and password once, with a copy button; the list below shows every database's name, user and secret, never its password.

From the namespace secret, in a deployment. Unless you asked it not to, creating a database also writes its credentials to the namespace secret pg-<name>, with keys url, host, port, database, user and password. A deployment reads it as an environment variable, so the connection string never passes through your code:

await lb.createDeployment({
  id: "api-acme",
  namespace: "acme",
  routes: [{ host: "api-acme.us5.heyo.work" }],
  vm: {
    driver: "firecracker",
    image: "my-api",
    port: 8080,
    env_from: [{ secret: "pg-app", key: "url", as: "DATABASE_URL" }],
  },
});

app-lb never hands secret values back to a client, which is why the secret is for deployments and the createDatabase response is for you. A password that is lost from both cannot be read back; create a new database, and delete the old one to revoke its credential.

lb.postgres("acme").databases() lists what exists, which secret each one's credentials are in, and whether it is live, for picking the right pg-<name> without going back to the console.