create_project
Create a new Hatchable project with a dedicated PostgreSQL database, URL slug, and project ID. Call this before writing files or creating tables.
Instructions
Create a new Hatchable project. This generates a URL slug, creates a dedicated PostgreSQL database, and returns the project ID and URLs. Call this first before writing files or creating tables.
Project structure
public/ static files, served at their file path
api/ backend functions — each file is one endpoint
hello.js → /api/hello
users/list.js → /api/users/list
users/[id].js → /api/users/:id (req.params.id — one segment)
docs/[...path].js → /api/docs/*path (req.params.path — string[], catches multi-segment)
_lib/ shared code, not routed
migrations/*.sql SQL files, run in filename order on every deploy
seed.sql optional — runs on first deploy / fork, once per project
hatchable.toml optional overrides (cron, auth, project name)
package.json dependencies (no build scripts yet — build locally, commit public/)Routing precedence
Most-specific wins. For a request to /api/users/42:
api/users/42.js(static) — beatsapi/users/[id].js(single-param,params.id = "42") — beatsapi/users/[...rest].js(catch-all,params.rest = ["42"])
Catch-all params arrive as string[], never slash-joined. Use req.params.path as an array:
const [first, ...rest] = req.params.path;
Static file resolution (public/)
A request to /foo/bar/baz tries, in order:
public/foo/bar/baz(exact file)public/foo/bar/baz.htmlpublic/foo/bar/baz/index.htmlAncestor
index.htmlfallback — walks up:public/foo/bar/index.html→public/foo/index.html→public/index.html
Step 4 means each folder with an index.html acts as its own mini-site. You can ship
an /admin/* React SPA alongside a static marketing page at / — unmatched paths
under /admin/ fall back to public/admin/index.html, not the root one.
Handler contract
Every file under api/ exports a default async function:
// api/users/list.js
import { db, auth } from "hatchable";
export default async function (req, res) {
const user = auth.getUser(req);
if (!user) return res.status(401).json({ error: "Not logged in" });
const { rows } = await db.query(
"SELECT id, name FROM users WHERE org_id = $1",
[user.id]
);
res.json(rows);
}
// Optional: restrict methods
export const methods = ["GET"];
// Optional: register this endpoint as a cron job
// export const schedule = "0 */6 * * *";req (Express-shaped)
method, url, path, headers, cookies, params, query
body — parsed by Content-Type: JSON → object, urlencoded → object, multipart/form-data → object of non-file fields
files — present for multipart uploads: [{ field, filename, contentType, buffer }]
res (Express-shaped)
res.json(data), res.status(code) (chainable), res.send(text|buffer)
res.redirect(url), res.cookie(name, value, opts), res.setHeader(name, value)
SDK — import from "hatchable"
db.query(sql, params) → { rows, rowCount }
db.transaction([{sql, params}, ...]) → { results: [{rows, rowCount}] }
auth.getUser(req) → { id, email, name } | null
email.send({ to, subject, html })
storage.put(key, buffer, contentType) → url
storage.get(key) → { buffer, contentType }
storage.del(key)That's the entire SDK. Everything else uses standard Node: fetch for
external HTTP, process.env.KEY for secrets (set with set_env), crypto/etc
from node:*.
Database
Postgres. Write schema in migrations/*.sql. Files run in filename order, tracked in __hatchable_migrations so each runs once.
Always use RETURNING to get inserted ids in the same round trip:
INSERT INTO users (email) VALUES ($1) RETURNING idNever call lastval() or LAST_INSERT_ID() — each db.query is a fresh connection, so session-local state doesn't carry across calls.
Available Node.js APIs and packages
Functions run in Node.js 20. The full hatchable SDK is always available. In addition, these packages are pre-installed and ready to import:
sharp, puppeteer-core (with Chromium at /usr/bin/chromium), csv-parse, csv-stringify, xlsx, bcrypt, jsonwebtoken, uuid, date-fns, lodash, marked, sanitize-html, cheerio, xml2js, archiver, qrcode, stripe, openai.
Standard Node.js APIs are available: fs, child_process, net, http, Buffer, stream, path, os, crypto, etc. External HTTP via global fetch(). Secrets via process.env (set with the set_env tool).
Visibility
Three tiers — each one a step up in who the software is for:
personal — free. You and anyone you invite. Login-gated via Hatchable accounts. Build anything including auth — test the full flow with your invitees before going live.
public — $12/mo. On the open web. Custom domains. No branding. No app-level auth (use Hatchable identity only).
app — $39/mo. On the open web + your app has its own users. Email/password signup, OAuth, password reset. If your project has [auth] enabled, this is the only live tier — you can't go Public with auth, you go straight to App.
Calling the API from public/
At deploy time, Hatchable injects a tiny bootstrap into every HTML file:
window.__HATCHABLE__ = { slug: "my-app", api: "/api" };Use it as the base URL:
const API = window.__HATCHABLE__.api;
fetch(API + "/users/list").then(r => r.json()).then(render);Auth (optional)
Enable auth in hatchable.toml to get a complete signup/login/session system with one config block. The platform auto-mounts /api/auth/* — do not write files under api/auth/ when auth is enabled.
[auth]
enabled = true
providers = ["email"] # or ["email", "google", "hatchable"]Auto-mounted endpoints:
POST /api/auth/sign-up/email — create account with email + password
POST /api/auth/sign-in/email — log in
POST /api/auth/sign-out — clear session
GET /api/auth/get-session — current session + user
POST /api/auth/forget-password — send password-reset email
POST /api/auth/reset-password — complete password reset
GET /api/auth/sign-in/social/:provider — OAuth flow (google, github)
GET /api/auth/hatchable/sso — one-click Hatchable SSO (when enabled)
Users live in these tables inside your project's own database: users, sessions, accounts, verifications
You can extend the users table with your own columns:
-- migrations/002_user_profile.sql
ALTER TABLE users ADD COLUMN phone text;
ALTER TABLE users ADD COLUMN tier text DEFAULT 'free';You CANNOT drop or rename users/sessions/accounts/verifications or create your own tables with those names — the deploy will fail with a clear error.
In your API functions, auth.getUser works the same whether auth is enabled or not:
import { auth, db } from "hatchable";
export default async function (req, res) {
const user = await auth.getUser(req); // NOTE: await when auth is enabled
if (!user) return res.status(401).json({ error: "Not logged in" });
const { rows } = await db.query(
"SELECT * FROM bookings WHERE user_id = $1",
[user.id]
);
res.json(rows);
}OAuth providers need credentials set via hatchable secret set:
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
Deploy
After writing files, call the deploy tool. It runs migrations, seeds
(first deploy only), copies public/ to the CDN, registers api/ routes,
and — if [auth] enabled — provisions the auth tables in your database.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | Human-readable project name (e.g. "My Booking App") | |
| visibility | No | Project visibility: personal (default, login-gated, free) or public ($12/mo, open web) | |
| description | No | Short project description |