Install
Create a Chaski project and deploy it as a Worker of its own, with two commands and Cloudflare Access.
Requirements
- A Cloudflare account, with Cloudflare Access to sign your team in to the dashboard.
- For email, the Workers Paid plan and a domain onboarded to Email Service.
- For iOS and macOS push, an APNs key from your Apple developer account. For Android, a service account key from your Firebase project. For Windows, a Microsoft Entra app registration with a client secret. Web push needs nothing: Chaski makes its own key pair.
This guide sets up the production Chaski; Environments adds staging and the rest.
Create the project
npx chaskijs@next initpnpm dlx chaskijs@next inityarn dlx chaskijs@next initbun x chaskijs@next init@next picks Chaski 0.1, in pre-release until it ships as latest. The command asks:
- Where the project lives: a folder of your app’s repository, such as
chaski/, or a new repository of its own. - The Worker’s name,
chaskiunless you pick another. Its resources take their names from it: the databasechaski, the queueschaski-deliveriesandchaski-deliveries-dlq, the Workflowchaski-scheduler. A Workflow’s name is unique in your account, so give each Chaski an account holds a name of its own, such asacme-chaski. - Email, on unless you say no, and email attachments, which add an R2 bucket.
- The URL where your app mounts
userRoutes(), for the pixel that tracks email opens. Leave it for later if your app doesn’t mount it yet.
Every answer has a flag, for scripts and agents:
npx chaskijs@next init chaski --folder --name chaski --attachments --public-url https://app.example.com/me/notifications--repo makes it a new repository, --no-email leaves email out and --skip-install only writes
the files. Without them, it installs the dependencies with the package manager you ran it with,
and creates the local database, so npx wrangler dev runs the dashboard right away:
Local development.
Read what it wrote
A Chaski project holds package.json, with chaskijs, hono and wrangler, a main module and
wrangler.jsonc, plus .dev.vars for wrangler dev and a .gitignore that keeps it out of Git.
The main module re-exports the Worker, and the classes its bindings name:
export { ChaskiEntrypoint, ChaskiEvents, ChaskiFeed, ChaskiScheduler, default } from 'chaskijs/worker';That line is all the code a Chaski project holds. New versions come through the package:
npx chaskijs upgrade installs the latest and updates the project to it.
wrangler.jsonc declares the bindings. The top level is production:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "chaski",
"main": "src/index.ts",
"compatibility_date": "2026-10-15",
"observability": { "enabled": true },
"vars": {
// Shown in the dashboard and returned by ready().
"CHASKI_ENVIRONMENT": "production",
// Where your app mounts userRoutes(); emails track opens through it.
"CHASKI_PUBLIC_URL": "https://app.example.com/me/notifications"
},
// wrangler dev signs this identity in, as Cloudflare Access does once deployed.
"access": {
"dev": { "aud": "chaski-local", "identity": { "email": "you@example.com", "name": "You" } }
},
"d1_databases": [
{
"binding": "CHASKI_DB",
"database_name": "chaski",
// chaskijs deploy creates the database and writes its id here.
"database_id": "local",
// Chaski's tables come from the package.
"migrations_dir": "node_modules/chaskijs/migrations"
}
],
"queues": {
"producers": [{ "binding": "CHASKI_QUEUE", "queue": "chaski-deliveries" }],
"consumers": [
{
"queue": "chaski-deliveries",
"max_batch_size": 10,
"max_batch_timeout": 1,
// Chaski counts on five retries.
"max_retries": 5,
"dead_letter_queue": "chaski-deliveries-dlq"
}
]
},
"durable_objects": {
"bindings": [
{ "name": "CHASKI_FEED", "class_name": "ChaskiFeed" },
{ "name": "CHASKI_EVENTS", "class_name": "ChaskiEvents" }
]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChaskiFeed", "ChaskiEvents"] }],
"workflows": [
{ "name": "chaski-scheduler", "binding": "CHASKI_SCHEDULER", "class_name": "ChaskiScheduler" }
],
"r2_buckets": [{ "binding": "CHASKI_ATTACHMENTS", "bucket_name": "chaski-attachments" }],
"send_email": [{ "name": "CHASKI_EMAIL" }]
}init writes vars.CHASKI_PUBLIC_URL only when you give it one, r2_buckets only with
attachments and send_email only with email. access.dev takes your git config user.email.
compatibility_date is the date Chaski’s own tests run on. Keep it, or move it to one no later
than your Wrangler’s workerd, which ships behind today: past that date, wrangler dev warns and
falls back.
The binding names are fixed: Chaski reads them by name. Each one turns on a part of it:
| Binding | Needed for |
|---|---|
CHASKI_DB (D1) | Required. Where Chaski keeps its data. |
CHASKI_QUEUE (producer and consumer) | Required. Every delivery goes through it. |
CHASKI_FEED (ChaskiFeed) | The in-app channel |
CHASKI_EVENTS (ChaskiEvents) | Live updates in the dashboard |
CHASKI_SCHEDULER (ChaskiScheduler) | schedule() |
CHASKI_EMAIL (send_email) | The email channel |
CHASKI_ATTACHMENTS (R2) | Email attachments |
Configuration lists the vars and the secret.
Deploy
In the project, signed in to Cloudflare with npx wrangler login:
npx chaskijs deployIt checks what the Worker is missing in your Cloudflare account, lists what it will do, and asks before it does any of it:
- Creates the resources that don’t exist yet: the database, the queues and, with
attachments, the bucket. It writes the database’s id into
wrangler.jsonc. - Applies Chaski’s migrations to the database. They go before the code, since a new version may need a new table, and they only add.
- Deploys the Worker, with a random
CHASKI_SECRETon its first deploy. The secret encrypts the push keys and credentials you save in the dashboard, such as your APNs key, so the database alone never holds them in the clear. It travels with the Worker’s first version, so the Worker never runs without one. A Worker that already runs without it gets one right after the deploy.
It prints the Worker’s URL, such as https://chaski.<your-subdomain>.workers.dev, which serves
the dashboard. Run it again after every change or upgrade: it creates only what’s missing, and
always migrates before it deploys. --yes answers its question, for a deploy job.
Each step is a Wrangler command, should you rather run them yourself, in this order. The first deploy uploads the secret with the Worker, from a file only you can read:
npx wrangler d1 create chaski # its id goes in database_id
npx wrangler queues create chaski-deliveries
npx wrangler queues create chaski-deliveries-dlq
npx wrangler r2 bucket create chaski-attachments # only with attachments
npx wrangler d1 migrations apply CHASKI_DB --remote
(umask 077; printf 'CHASKI_SECRET=%s\n' "$(openssl rand -base64 32)" > chaski-secret.env)
npx wrangler deploy --secrets-file chaski-secret.env
rm chaski-secret.envTo give a Worker that already runs a secret, deploy first, then
openssl rand -base64 32 | npx wrangler secret put CHASKI_SECRET: until the Worker exists,
secret put creates an empty one under its name.
Turn on Cloudflare Access
The dashboard opens only for people Cloudflare Access signs in; until Access covers its URL, it
answers Chaski needs Cloudflare Access. In the Cloudflare dashboard, open the chaski Worker,
go to Settings › Domains & Routes and select Enable Cloudflare Access on its
workers.dev URL. Then choose who gets in in the Access application it creates, under Zero
Trust › Access › Applications.
Anyone Access lets in uses the whole dashboard: templates, replays, channel settings and pauses. Each change is recorded under the email Access signed in.
Set up the channels
Open the dashboard and go to Channels. Every channel starts off, and turns on once you save its settings there: the sender address for email, the in-app feed, and a key pair for web push or the credentials of APNs, FCM and WNS. Channels covers what each one takes.
Connect your app
Your app reaches Chaski over a service binding, to send and to serve its frontend the signed-in user’s feed, devices and preferences: Connect your app. Then Templates is where your first notification type begins.
Updated Oct 30, 2026
MIT License · chaski.sh