Skills
The Agent Skill that teaches AI coding agents to use Cradler. Copy the full doc to paste into your AI tool.
Cradler
Cradler gives an app a backend — a database and file storage — through one typed TypeScript SDK. Use it whenever the app you are building needs to persist data or store uploaded files.
The most important thing to know: you never design a schema, write SQL, or
run a migration. You just write data, and Cradler creates the tables and
columns for you. Do not generate CREATE TABLE statements, schema files,
ORM models, or migrations — that is not how Cradler works, and it will only
confuse the user.
Setup
The user needs a Cradler project. If they do not have one, tell them to
create one for free at https://cradler.ai. From the project dashboard they
can copy three things — ask the user for them:
- API URL — the data gateway URL, e.g.
https://gateway.cradler.ai - Project ID — the project's slug
- API keys — an
anonkey and aservicekey
Install the SDK:
pnpm add @cradler/sdk
Create the client. Read the keys from environment variables — never hardcode them:
import { createClient } from "@cradler/sdk";
const cradler = createClient({
url: process.env.CRADLER_API_URL!,
projectId: process.env.CRADLER_PROJECT_ID!,
apiKey: process.env.CRADLER_API_KEY!,
});
Which key to use
servicekey — full access. Use it only in server-side code (API routes, server actions, backend jobs). Never send it to the browser.anonkey — safe for client-side / browser code. What it can do is limited by the table permissions the user sets in the dashboard.
If the app talks to Cradler directly from the browser, use the anon key
there, and tell the user to enable the table permissions it needs in the
dashboard.
Storing and reading data
A "collection" is a table. You do not create it — it appears the first time
you write to it. Use plain camelCase field names; the SDK maps them to the
database for you.
Insert
// The "posts" table and its columns are created automatically.
await cradler.from("posts").insert({
title: "Hello world",
body: "My first post",
published: true,
});
// Insert many at once:
await cradler.from("posts").insert([{ title: "A" }, { title: "B" }]);
Writing a field that does not exist yet adds the column automatically.
Every row automatically gets three Cradler-managed fields — id,
createdAt, and updatedAt. Read them and filter or order by them, but
never pass them to insert() or update() — Cradler always sets them.
Query
// All rows:
const { rows } = await cradler.from("posts").select();
// Specific columns, filtered, ordered, paginated:
const { rows, count } = await cradler
.from("posts")
.select("id", "title")
.eq("published", true)
.order("createdAt", { desc: true })
.limit(20);
// Just the first match, or null:
const post = await cradler.from("posts").select().eq("id", id).first();
Filters — chain as many as needed: .eq .neq .gt .gte .lt .lte
.like(field, pattern) .ilike(field, pattern) .in(field, [...])
.isNull(field) .notNull(field).
Update and delete
await cradler.from("posts").update({ published: false }).eq("id", id);
// delete() requires at least one filter — it can never wipe a whole table.
await cradler.from("posts").delete().eq("id", id);
Every insert / query / update / delete resolves to { rows, count }.
Files and images
// Upload — body is a Blob, ArrayBuffer, or string.
await cradler.storage.upload("avatars/cat.png", fileBlob, {
contentType: "image/png",
});
// A temporary signed URL to display or download the file:
const url = await cradler.storage.getUrl("avatars/cat.png");
// Download as a Blob:
const blob = await cradler.storage.download("avatars/cat.png");
// List (optionally by prefix) and delete:
const files = await cradler.storage.list("avatars/");
await cradler.storage.remove("avatars/cat.png");
When the upload happens in the browser, turn on image compression.
Pass { compress: true } and the SDK will shrink and re-encode the image
on the device before it is sent — typically 90%+ smaller than a raw phone
photo. Non-image files (PDFs, zips, etc.) pass through unchanged, so it
is safe to set on every browser-side upload:
const { path } = await cradler.storage.upload("avatars/cat.jpg", file, {
compress: true,
});
// `path` is now "avatars/cat.webp" — the extension follows the new format.
// Save *this returned path* in the database, not the original.
Do not set compress: true in Node / server-side code — it only works in
the browser. In server code, upload the bytes as-is.
When you save a file reference in the database, store the path returned
by upload(), not the URL. URLs from getUrl() are short-lived signed
URLs that expire; persisting them leads to broken links. Resolve the path
to a fresh URL with getUrl() when you need to display it.
Errors
Failed calls throw a CradlerError with an HTTP status and a message:
import { CradlerError } from "@cradler/sdk";
try {
await cradler.from("posts").insert({ title: "x" });
} catch (err) {
if (err instanceof CradlerError) {
console.error(err.status, err.message);
}
}
Rules
- Never write SQL, schema files,
CREATE TABLE, ORM models, or migrations. Just write data — the schema evolves on its own. - Never put the
servicekey in client-side / browser code; use theanonkey there. - Do not hardcode keys — read them from environment variables.
- Use
camelCasefield names. id,createdAt,updatedAtare managed by Cradler — read them, but never include them in aninsert()orupdate().delete()always needs at least one filter.
Related
To read and write a project's data directly from an agent — operating on
the data rather than writing app code — Cradler also offers an MCP server,
@cradler/mcp.