Why
Why Cubby exists
Coding agents can now write a complete web app from one sentence. Hosting it is the hard part: the app needs a database, logins, somewhere to live, and a URL your friends can open. Hosted builders like Lovable solve that, but your apps and data live on someone else's platform, and you can't use the agent you already like.
Cubby is the self-hosted version for a group of people who trust each other: a family, a team, a group of friends. One person runs a small server. Everyone else uses whichever agent they already have.
| Cubby | Hosted builders | A VPS by hand | |
|---|---|---|---|
| Who owns the server and data | You | The platform | You |
| Use your own agent (Claude Code, Codex, Cursor…) | Yes | Their agent only | Yes |
| Database, logins, file uploads, realtime per app | Built in | Built in | You set it up per app |
| Deploy is one command the agent runs itself | Yes | Yes | No |
| Friends get their own accounts and apps | Yes | Separate accounts | Shared SSH, or nothing |
| Cost | One small VPS | Per-seat plans | One small VPS |
Architecture
How it works
Everything runs in one Docker container: Caddy in front for HTTPS, the Cubby server, and a shared PocketBase database. Each app gets its own subdomain and its own walled-off slice of the database.
One subdomain per app
Each app is its own website, so apps can never read each other's logins in the browser.
One database, walled off
App polls owns collections named polls__*. The proxy refuses anything else.
Static frontends, scheduled jobs
Apps are Vite + React + Tailwind builds; security lives in database rules. Code that runs on a schedule goes in sandboxed jobs.
Admin · 5 minutes
Install a server
You need a Linux VPS (Ubuntu or Debian work best) with ports 80 and 443 open (an IP-only install can use another port). 1 vCPU and 1 GB of RAM is enough for a group of friends. Run:
curl -fsSL https://raw.githubusercontent.com/chaitanyadukkipaty/cubby/main/install.sh | sudo bash
The installer asks four questions, then installs Docker if needed, builds the image and starts Cubby:
| Question | What to answer |
|---|---|
| Domain | e.g. cubby.example.com, or leave blank to use the server's IP address |
| Admin email | Your email. You'll log in to the dashboard with it. |
| Admin password | At least 8 characters. It isn't stored on disk after setup. |
| Name your cubby | What everyone sees, e.g. Priya's Cubby. Press Enter to use the suggested name; you can rename it later. |
When it finishes it prints your URL and a signup code like otter-maple-42. Those two things are all your friends need.
Domain or IP?
| Mode | App URLs | HTTPS |
|---|---|---|
| Domain (recommended) | https://polls.cubby.example.com | Automatic, free certificates per app |
| IP only | http://polls.203-0-113-5.sslip.io | No. Passwords and tokens travel unencrypted. |
For a domain, add two DNS A records pointing at the server before installing: one for cubby.example.com and a wildcard for *.cubby.example.com. Started on an IP? Switch any time:
sudo cubby-server set-domain cubby.example.com
Port 80 already in use?
If another web server (nginx, Apache, another container) holds port 80, an IP-only install tells you what holds it and offers a free port instead, like 8080. Press Enter to take it. Your URL becomes http://203-0-113-5.sslip.io:8080 and apps http://polls.203-0-113-5.sslip.io:8080; open that port in your cloud provider's firewall. Pass --http-port 8080 to choose it up front.
The port is part of every URL your friends and their agents have saved, so it never changes on its own: if something else takes it later, cubby-server upgrade and rebuild stop and say what to free. A domain always needs ports 80 and 443, because Let's Encrypt only checks those; the installer and set-domain refuse until they're free.
Which version gets installed?
The latest release, unless you choose. Pass options after bash -s --:
curl -fsSL https://raw.githubusercontent.com/chaitanyadukkipaty/cubby/main/install.sh | sudo bash -s -- --version 0.1.1 # a specific release curl -fsSL https://raw.githubusercontent.com/chaitanyadukkipaty/cubby/main/install.sh | sudo bash -s -- --ref my-branch # any branch, tag or commit
A --ref install isn't a release, so it hasn't been through release testing; use it to try a fix early. To install without any prompts, answer the questions with --domain (or --no-domain), --admin-email, --admin-password-file and --name. --http-port and --pb-port pick the host ports when the defaults (80 and 8090) are taken. --no-fail2ban skips the scanner bans.
Admin
Invite people
Send your friends the server URL and the signup code. On the dashboard's Admin page, Copy invite copies both. They open the URL, sign up with the code, and land on Home, under your server's name.
The dashboard has a sidebar (a bottom bar on phones). The first time, Home walks them through connecting their agent in three steps; after that it's a timeline of what their agents did, with suggestions for what to ask next. Agents lists the agents they connected, Apps has a row per app, Explore shows everyone's apps, and admins also get Admin. Every page has its own link, so a bookmark or a reload lands in the same place. It follows the system's light or dark setting, and the toggle under your email overrides it. Press ⌘K (Ctrl+K), or click Search in the sidebar, to jump to any page or app, copy the setup prompt or a common command, switch theme or log out, all from the keyboard.

Rotate code invalidates the old code for new signups; existing accounts keep working.
Open signup drops the code requirement entirely. Leave it off for a server on the public internet: bots find open signup forms within days.
Signup and login are rate-limited to 10 attempts a minute per IP address.
Everyone · once
Connect your agent
Each person pastes one prompt into their coding agent. It's on their dashboard's Home page the first time (and behind Connect a new agent on the Agents page later), with the server's name and URL filled in:
I'd like you to set up Cubby: Priya's Cubby at https://cubby.example.com, a self-hosted server where you build and host web apps for me. Install it as a skill if I have npm: npx skills add chaitanyadukkipaty/cubby --skill cubby -g If not, do this instead: curl -fsSL https://cubby.example.com/skill.sh | sh After installing, read ~/.agents/skills/cubby/SKILL.md and follow its Setup to connect to https://cubby.example.com (you'll send me a link to approve), review the docs at https://cubby.example.com/docs.md and ask me what I'd like to build.
The agent installs the cubby skill, instructions that teach it how to use Cubby, with npx skills add from GitHub (or the server's own /skill.sh without npm), sends you a link to approve, and adds the server to your list by name. It's a prompt, not a bare curl … | sh, so your agent knows what you're asking for instead of flagging an unknown script. Giving your agent just the server URL works too: anything that isn't a browser gets the setup steps as plain text from the root page. The skill goes in ~/.agents/skills/cubby and is linked into every agent it finds:
Claude Code
~/.claude/skills
Codex
~/.codex/skills
Cursor
~/.cursor/skills
OpenCode
~/.config/opencode/skills
The skill is short on purpose. It tells the agent to read the server's live /docs.md first, so limits and rules always match the server you're on.
Belong to more than one cubby?
Run each server's command once. They all share one skill and one cubby command, and your agent knows them by name: say “build it on Acme Cubby”, and if you have several and don't say, it asks once. Each app remembers which server it lives on, so later changes always go to the right place.
Servers can run different versions of Cubby. The cubby command handles that for you: for each server it uses a CLI that matches that server's version, downloading it once into ~/.cubby/cli/ when needed.
The first time your agent connects
The agent installs the cubby CLI from your server by itself, then sends you one message:
To connect me to your Cubby account, open this link and click Approve (you can sign up there if you're new).
You open the link, check the code matches, and click Approve. The page says which agent is asking, like “Claude Code on chaitanya-mbp wants access”. The agent carries on by itself. You never paste passwords or tokens into a chat.

Every login is its own agent
Each approved cubby login gets its own key and a name from the agent tool it runs under (Claude Code, Codex or Cursor, otherwise “Terminal”; --client sets it) and the machine it's on. Connect Claude Code on your laptop and Codex on your desktop and Cubby sees two agents, not one shared token. Agents that ran an older CLI get a dated name like “Agent · 2026-10-17”.
cubby agents # ID NAME TOOL MACHINE CREATED LAST SEEN # k0m2c8d1x4e5f6g Manual token - - 2026-10-01 2 days ago # * r8a2kq0m1vx7c3d Claude Code · chaitanya-mbp Claude Code chaitanya-mbp 2026-10-17 just now # p3n7w9s2t5u8v1y Codex · desktop Codex desktop 2026-10-17 3 hours ago
The * marks the agent asking. The dashboard's Agents page shows the same list, with the apps each agent touched, and opening one shows everything it did. The token for scripts and CI (also on the Agents page) shows up as Manual token; tokens from before agents existed keep working and appear there too. Last seen is when the agent last used its key, updated at most once a minute.
Rename or revoke an agent
Done with an agent, or lost the laptop it ran on? Revoke it, and its key stops working on its very next request. Click Rename or Revoke next to it on the Agents page, or let any of your agents do it for you: just ask.

cubby agents rename p3n7w9s2t5u8v1y "Old desktop"
cubby agents revoke p3n7w9s2t5u8v1y --yes # refuses without --yesAn agent can revoke itself too; the CLI then forgets the dead token, like cubby logout. You can only see, rename and revoke your own agents. Regenerate on the Agents page replaces only your manual token; agents keep their own keys.
See which agent did what
Your dashboard's Home is a timeline of it: one line per change, grouped by day, with a badge for the agent that made it. The deploy that's live has Roll back (after a confirm, the deploy before it goes live again), and a failed job or an app with many API errors gets Copy fix-it prompt, a ready-made message for your agent naming the app and where to look. Above it, “What should your agent do next?” offers prompts to copy, and beside it you see who's on your apps right now, views over the last 7 days and your agents. Home only shows your own apps; friends' apps are on Explore.

The same log is in the terminal:
Cubby keeps a short log of every change: creating, hiding and deleting apps, deploys (with the deploy id and file count), rollbacks, schema pushes and drops, secrets set or removed (by name, never the value), jobs run, paused or resumed, agent logins, and admin actions on users, apps and backups. Each entry says which agent made it, or dashboard for the dashboard and the manual token. Reads aren't logged, and entries are pruned after 30 days.
cubby activity friend-polls # 2026-10-17 09:30 friend-polls Claude Code · chaitanya-mbp Deployed k3x9q2 (14 files) # 2026-10-17 09:29 friend-polls Claude Code · chaitanya-mbp Added secret EMAIL_API_KEY # 2026-10-16 18:02 friend-polls dashboard Updated "friend-polls": made it unlisted
Leave out the app to see everything you and your agents did. --agent <id> (ids are in cubby agents --json) or --agent dashboard narrows it to one; --limit sets how many (50 by default, up to 500). You only ever see your own entries; admins can see everyone's with --all. An app's history stays after the app is deleted until it ages out, and deleting a user deletes theirs.
Everyone
Build an app
Just ask, in plain words. For example:
Build me an app where my friends can create polls and vote on them, and host it on Cubby.
The skill tells the agent to work like Lovable: decide the details itself, build something complete and polished, ship early, and report back in plain language. A typical run:
- Picks a name, then runs
cubby init friend-pollsto scaffold Vite, React, Tailwind and the database client. - Designs the data model and its security rules in
cubby/schema.json. - Writes the UI, then ships with
cubby deploy: schema, build and upload in one step. - Tests its own rules, then tells you the URL, what it built, and ideas for next steps.
Everyone on your server can open the app and sees it on the Explore page. Ask for it to be unlisted if you'd rather share only the link.

Scheduled jobs
Some things should happen without anyone opening the app: closing polls when they expire, a weekly digest, a reminder before a deadline, pulling prices from an API every hour. Ask for it the same way:
Close polls automatically when they expire, and email everyone a digest of new polls every Monday.
The agent writes one file per job in the app's cubby/jobs/ folder, with a schedule in cron syntax (always UTC):
// cubby/jobs/close-expired-polls.ts export const schedule = '*/15 * * * *'; // every 15 minutes export default async ({ pb, env, log, fetch }) => { const now = new Date().toISOString().replace('T', ' '); const expired = await pb.collection('polls').getFullList({ filter: pb.filter('closed = false && closes_at < {:now}', { now }) }); for (const poll of expired) await pb.collection('polls').update(poll.id, { closed: true }); log(`closed ${expired.length} polls`); };
- Shipped with the app.
cubby deploybundles the jobs with the frontend, andcubby rollbackbrings back the jobs of that deploy. - Its own data, past the rules. A job's
pbacts as admin, but only for that app's collections. Other apps and the server's own data are out of reach. - Secrets stay secret. API keys are set with
cubby env set NAME=value, stored encrypted, never shown again, and given only to the app's jobs asenv.NAME. - Checked right away.
cubby jobs run <job>runs one now and prints its log, so the agent tests each job before telling you it's done. The Jobs tab of the app's page on the dashboard lists its jobs, with Run now, Pause and the last log, and the Env tab sets and removes secrets by name (values are never shown). - Safe to miss. A run that's due while the previous one is still going is skipped, and runs missed while the server was down aren't caught up later.

Visitors, activity and API health
Once friends are using an app, you'll want to know if they actually are. Ask:
How many people used the polls app this week?
The agent runs cubby metrics friend-polls --range 7d and answers from what the server counted:
friend-polls: 214 views, 23 visitors in the last 7d
Active now 2, peak 7
API: 1480 requests, 3 errors (0.2%)
Accounts: 19, 4 new
Top paths: / 120 · /polls/7 41 · /new 30 · other 23
Deploy 2026-10-14 18:02 k3x9q2 add comments
2026-10-11 18 views 4 visitors 1 signups
2026-10-12 41 views 9 visitors 2 signups
…- Nothing to add to the app. The server counts as it hands out the app's pages: every page a browser loads is a view. Scripts, images, database calls and obvious bots and link previews don't count.
- Visitors, not people's IPs. A visitor is a hash of the IP address and browser, mixed with a secret that only lives in memory and changes every day (UTC). The same person counts once a day and can't be followed from one day to the next. IP addresses are never stored.
- Views by the hour, visitors by the day. Counts are written to the database every minute, so a crash loses at most a minute, and a restart may count some of that day's visitors twice. Visitors are unique per day; over several days, the total adds up each day's visitors. A range that starts mid-day, like
1h, counts visitors and top paths from the start of that UTC day, and says so. - Who's on it now. Active now counts visitors who loaded a page or used the database in the last 5 minutes, without a heartbeat or anything added to the app. It lives only in memory; the highest it reached each hour is kept as that hour's peak, and the report shows the biggest peak in the range.
- Is the backend healthy? Every database call through
/pbcounts as an API request, and every 5xx answer as an error, per hour. The error rate is errors over requests. - Accounts. The number of accounts in the app's own users collection, and how many signed up each day, read from the database when you ask.
- Charted on the app's page. The dashboard's app page opens on Overview: active now, views, visitors, API error rate and accounts across the top, then views, unique visitors, active visitors and API errors for the last 24 hours, 7 days or 30 days. The charts share one time axis and every deploy is a dashed line through all of them, so a spike right after a deploy stands out. Each chart has a text summary for screen readers and a Show as table fallback. The Apps list shows each app's visitors right now, a 7-day sparkline and which agent deployed it last.
- Which pages. Each day keeps its 10 most viewed paths (query strings dropped, so nothing in them is stored), and everything else as "other". The report adds them up over the range.
- Did my deploy break it? Deploys in the range are listed with the numbers, so a jump in errors lines up with the deploy that caused it. The app guide tells agents to run
cubby metrics <app> --range 1hafter deploying and look at the errors. - A year of history, kept small. Hourly numbers are kept 30 days; the nightly maintenance then folds them into their day, and days are kept a year. Ranges up to
30dare reported by the hour; longer ones (up to1y) by the day. - Yours to see. Only the app's owner (and the server's admins) can see an app's numbers.
cubby metricswithout an app lists all of yours;--rangetakes1h,24h,7d,30d,1yor any hours or days in between, and--jsonadds views, peaks, API requests and errors per hour, views, visitors, API requests and errors per day, and signups per day. Admins get every app summed, with the busiest apps, fromcubby metrics --all.

Its other tabs: Activity (that app's timeline), Deploys (every kept deploy with Roll back to this), Jobs, Env and Settings (show in the gallery, and delete after typing the app's name). The tab is part of the link, like /apps/friend-polls/deploys, so a reload keeps it, and on a phone the tabs scroll sideways. A tab with nothing in it yet suggests what to ask your agent, with a copy button.
Data
Clear test data
Before you share an app, you'll want the test records gone without deleting the app or its accounts. Ask:
Clear out the test polls and votes in friend-polls, but keep the users.
The agent looks first, then empties just those collections:
cubby data list friend-polls friend-polls: 3 collections polls base 42 records files 1.2 MB users auth 5 records files 0 B votes base 310 records files 0 B cubby data clear friend-polls --collections polls,votes --yes Emptied polls (42 records), votes (310 records) in friend-polls. Snapshot 20261017T101500-a1b2c3 keeps them until 2026-10-24.
- Empty or drop. Emptying (the default) deletes every record and keeps the collection with its fields and rules.
--dropremoves the collection too, and the nextcubby deploypushesschema.jsonagain. - Always a snapshot first. Cubby first works out everything PocketBase will delete along with it (records pointing at a deleted one by a cascading relation, however many hops away, self-relations included) and the records that stay but lose a link. All of it is written to
DATA_DIR/snapshots/<app>/first: the collections' schema, the deleted records, the unlinked ones as they were, and the uploaded files of everything deleted, cascades included, up to 100 MB in all (above that, you're warned they can't come back). Snapshots are plain files kept for 7 days, removed by the nightly maintenance and with the app, and counted in the admin storage breakdown. - Refused rather than half done. A record pointing at one you clear by a required relation that doesn't cascade makes PocketBase refuse the delete. The preview names that relation and the records, and the clear is refused (409) before anything is snapshotted or deleted: delete or relink them first, or clear their collection too. Clears, restores and schema changes of one app run one at a time.
- Or just some records.
--collection polls --filter 'closed = true'(a PocketBase filter) or--ids a,bdeletes only those, and only they go in the snapshot.--dry-runshows "matches 9 of 42", five of the records, what would go by cascade or lose a link and anything blocking it, deleting nothing; a clear without--yesshows the same preview. More than 10,000 matches need--all-matching. A filter runs as the server, so it gets the same check as the app's own requests: naming another app's collection, or a platform one, is refused. - Accounts only when named. The app's users collection is only cleared when you list it, and the result warns that restored accounts get new passwords: PocketBase never hands out password hashes, even to the server.
- Owners only. Only the app's owner (and admins) can clear its data, the CLI needs
--yes, and every clear is incubby activity.
In the dashboard
The app's Data tab does the same, with the same guards:
- The collections with their records and uploaded files, all ticked except the app's user accounts (ticking those shows the new-passwords warning).
- Empty, Drop, or Delete records of one collection by a filter: type one, click an example, or pick a "created before" date.
- Preview is required: "matches N of M", up to five of the records (tick some to delete only those), what goes by cascade, what blocks the clear (the button stays locked), and an extra tick above 10,000 records. Changing the form makes the preview stale.
- Type the app's name to unlock the button. The result names the snapshot, which is listed below with Restore (dry run first, with any schema drift and an Overwrite option) and Purge.

Undo a clear
Cleared the wrong thing? Within 7 days, ask your agent to bring it back:
cubby data snapshots friend-polls
cubby data restore 20261017T101500-a1b2c3 --dry-run
cubby data restore 20261017T101500-a1b2c3
Restored snapshot 20261017T101500-a1b2c3:
polls: 42 restored
votes: 310 restored- Same ids, nothing doubled. Records come back with their original ids, so links still work. Ids that exist again are skipped, so restoring twice is harmless;
--overwrite --yesreplaces them, after saving the current versions in a new snapshot. - Links come back too. Records that stayed but lost a link get the removed ids back, in place, once those records exist again. Nothing else on them changes: ids added since stay, and a single relation set to something else since is left alone.
- Schema changes since. A dropped collection is recreated from the snapshot, rules included, whatever it points at; a wipe brings back every collection, also those that had no records. Fields removed since are skipped; a field whose type changed stops the restore until it's dropped or renamed.
--dry-runruns the same checks first. - Admins too, never silently. An admin can clear any app's data (Admin → Apps → Clear data, or the CLI), but must give a reason (
--reason). The owner sees it in their own activity ("Admin (priya@…) emptied polls — 1,204 records · snapshot … Reason: spam flood"), at the top of their Home, and as a banner on the app's page that links to its snapshots, until they dismiss it. Owners can restore an admin's snapshot but not purge it.--wipedrops every collection, and the app's nextcubby deployrecreates them fromschema.json. - What comes back different. PocketBase sets
createdandupdateditself and renames uploaded files, and it never hands out password hashes, not even to the server. So restored accounts get new random passwords, listed by the restore for you to hand out.
Reference
CLI reference
Your agent runs these for you. They're also handy by hand or in CI. Every command accepts --json.
| Command | What it does |
|---|---|
cubby login <server> | Log in by approving a link in the browser; each approval is a new named agent. --no-wait prints the link and exits; --wait finishes. |
cubby login <server> --token <t> | Log in with the token from the dashboard (scripts and CI). Or set CUBBY_TOKEN. |
cubby servers | List the servers you've added, with the default marked. |
cubby use <name|url> | Make a server the default for new apps. |
cubby whoami | Show the account and server you're using. |
cubby agents | List your agents (one per approved login) with tool, machine, date and last seen; * marks this one. |
cubby agents rename <id> <name> | Give an agent a name you'll recognize. |
cubby agents revoke <id> --yes | Revoke an agent's key at once; the others keep working. Revoking this agent logs it out here. |
cubby activity [app] [--agent <id>] [--limit n] | What changed in the last 30 days, newest first, and which agent (or the dashboard) did it. --agent dashboard for the dashboard's changes; --all shows everyone's (admins). |
cubby logout [--server <name>] [--all] | Forget one server (the current one by default) or all of them. |
cubby version | Show which CLI version runs for the current server, and the server's version. |
cubby help [<command>] [--json] | Every command and flag the targeted server supports. Agents use --json. |
cubby init <app> | Create an app: a folder from the template plus the app on the server. |
cubby deploy -m "…" | Push the schema if it changed, build (with the jobs in cubby/jobs/), and publish. --unlisted / --listed control the gallery. |
cubby deploys | List the last 10 deploys of this app. |
cubby rollback <id> | Serve an earlier deploy again, instantly, with the jobs it shipped. Data and secrets are not rolled back. |
cubby jobs | List the app's scheduled jobs with their schedule, next run and last run. |
cubby jobs run <job> | Run a job now and print its log. Exits with 1 if it failed. |
cubby jobs logs <job> [--limit n] [--run <id>] | Show past runs and their output (the last 100 per job are kept). |
cubby jobs pause | resume <job> | Stop or restart a job's schedule. Stays that way across deploys. |
cubby env list | set NAME=value | unset NAME --yes | Secrets for jobs. set NAME --from-stdin keeps the value out of shell history. |
cubby metrics [<app>] [--range 7d] [--all] | Views, visitors, active now and peak, API requests and errors, top paths, deploys, accounts and signups of one app, or a summary of all your apps. The last 24 hours by default, at most a year. --all: every app on the server, for admins. |
cubby schema push | pull | Apply or download the data model. Push never deletes anything. |
cubby schema drop <c>[.<f>] --yes | Delete a collection or field and its data. |
cubby data list [<app>] | The app's collections with their record counts and uploaded file sizes. |
cubby data clear [<app>] --collections a,b [--drop] --yes | Empty collections (fields and rules stay) or drop them, after a snapshot kept for 7 days. |
cubby data clear [<app>] --collection c --filter "…" | --ids a,b [--dry-run] --yes | Delete only matching records. --dry-run shows matches N of M, samples and cascades. |
cubby data snapshots [<app>] | Snapshots of clears: who, why, what, how many records, until when. |
cubby data clear [<app>] --wipe --yes | Drop every collection of the app (after a snapshot); the next deploy pushes the schema again. |
cubby data purge <id> --yes | Delete a snapshot before it expires. Owners can't purge one an admin made. |
cubby data restore <id> [--overwrite --yes] [--dry-run] | Bring a snapshot back. Existing ids are skipped (or replaced with --overwrite); restored accounts get new passwords. |
cubby projects · info · link <app> | List your apps, show the current one, or attach this folder to an existing app. |
cubby delete <app> --yes | Delete an app, its data and deploys. |
cubby admin storage | Admins only: disk use by category (database, uploads, current and older deploys, backups) and per app, with free disk. |
cubby admin backups [create | delete <date> --yes | keep <n>] | Admins only: list backups by date, back up now, delete one (both files; the only backup also needs --leave-none), or set how many nightly backups to keep (1 to 90; --yes when that deletes some). |
cubby admin deploys <app> [prune [--keep n] | delete <id>] --yes | Admins only: list an app's deploys, delete older ones oldest first (sparing the newest n) or one by id. Never the deploy the app is serving. |
cubby admin bans [unban <ip> --yes] | Admins only: IPs fail2ban banned (now, with the hosts and paths they swept, and 90 days back); lift a ban, logged in the activity feed. |
cubby skill install | remove | status | Manage the agent skill. login installs it too. |
--server <name|url> | On any command: which server to use. Otherwise it's CUBBY_SERVER, then the app's cubby.json, then the default. |
Installing the CLI by hand needs Node.js 20 or newer:
npm i -g https://cubby.example.com/cli.tgz
The CLI always comes from your own server, so its version matches the server. It warns you if they drift apart.
Reference
Data and security rules
Each app describes its data in cubby/schema.json. Names are written without the app's prefix; Cubby adds it.
{
"collections": [
{ "name": "users", "type": "auth",
"fields": [{ "name": "name", "type": "text", "max": 100 }],
"createRule": "", // anyone can sign up
"viewRule": "id = @request.auth.id" },
{ "name": "tasks", "type": "base",
"fields": [
{ "name": "title", "type": "text", "required": true },
{ "name": "done", "type": "bool" },
{ "name": "owner", "type": "relation", "collection": "users", "maxSelect": 1 }
],
"indexes": [{ "columns": ["owner"] }],
"listRule": "owner = @request.auth.id", // you only see your own tasks
"createRule": "@request.auth.id != '' && @request.body.owner = @request.auth.id",
"updateRule": "owner = @request.auth.id",
"deleteRule": "owner = @request.auth.id" }
]
}Rules are the security
A missing rule means nobody can do that action from the app. "" means anyone, even logged out.
Push never deletes
Fields you remove from the file are kept, with their data, until you run cubby schema drop.
Types don't change in place
Changing bool to text is refused. Add a new field instead.
Field types: text, editor, number, bool, email, url, date, autodate, select, file, relation, json, geoPoint. Rule syntax follows PocketBase API rules.
Admin
Running the server
The Admin page has a tab for each part of the server, and each tab has its own link (/admin/storage), so a reload keeps your place:
- Overview: users, views, visitors, who's on any app now, the API error rate and free disk; every app's views charted over 7 days; the busiest apps and the most active agents across every user; the disk at a glance; updates
- Users: disable someone (their apps stay online) or delete them with their apps
- Apps: every app with its owner and size; list and delete an app's older deploys; clear an app's data (with a reason the owner sees); delete an app
- Agents: every agent that changed something in the last 30 days, whoever it belongs to, with how much it did and where (owners rename and revoke their own)
- Jobs: every app's scheduled jobs and how their last run went, the time limit per run (60 seconds by default), and a switch that stops them all
- Settings: the server's name (shown on the dashboard, the sign-in page and when people connect an agent), the signup code or open signup, apps per user, the maximum deploy size, and how many nightly backups to keep
- Storage: where the disk goes, backups and snapshots (below)
- Security: IPs fail2ban banned for scanning or login brute force, with Unban, and 90 days of history (below)
Admins can't deploy into or edit other people's apps.

On the server
| Command | What it does |
|---|---|
cubby-server status · logs · restart | Check on it |
cubby-server upgrade | Back up, then move to the latest release (or keep following the branch you installed from). Data is kept. |
cubby-server upgrade --version <X.Y.Z> | Move to a specific release, including rolling back |
cubby-server upgrade --ref <ref> | Run any branch, tag or commit (not a release) |
cubby-server version | What's running, what it follows, and the latest release |
cubby-server code | Show the current signup code |
cubby-server backup · backups | Back up now, or list backups (from your own machine: cubby admin backups, below) |
cubby-server restore <YYYY-MM-DD> | Restore all apps and accounts to that day |
cubby-server reset-password <email> <pw> | Reset anyone's password |
cubby-server set-domain <domain> | Move from IP to a domain with HTTPS |
cubby-server fail2ban status · unban <ip> | What Cubby's jails banned; lift a ban (works even when Cubby is down) |
cubby-server fail2ban enable · disable | Install fail2ban and Cubby's jails (servers installed before it existed run this once); remove the jails and the access log |
Updates. Once a day the server asks GitHub for the latest Cubby release. When there's a newer one, the Admin page shows Update available with a link to the release notes and the command to run. Turn the check off in the Admin page, or set CUBBY_UPDATE_CHECK=0 in /opt/cubby/cubby.env to disable it completely.
Versions. A server built from a release shows its version, like 0.1.1. Anything else shows the commit too, like 0.1.1-dev+b2cb75c, so you always know which code is running.
Backups run every night at 03:00: a full database backup plus all deployed sites with their jobs, and the key that decrypts app secrets. The newest 7 are kept by default (see below to change it). The same nightly run first prunes activity log entries older than 30 days. Restoring rolls back every app on the server, so it's for real accidents.
Disk and backups
You don't need to SSH in to see where the disk goes or to free some of it. Admin → Storage shows it as one bar: the database, uploaded files, the deploys apps serve, older deploys kept for rollback, backups, data-clear snapshots, what isn't Cubby's, and free space, with every app's share in a table below.
- Back up now takes a backup right away (one at a time, shared with the nightly run).
- Delete on a backup says which two files go and how much space that frees. It warns before deleting the newest, and asks again before deleting the only one.
- Each backup has its
sudo cubby-server restore <date>command to copy: restoring stays on the server. - Snapshots left by data clears can be purged early.
- Settings → Backups shows what keeping a different number of backups would delete and free, or add, before you save it.

Logged in as the admin, you or your agent can do the same from your own machine:
cubby admin storage # database, uploads, current and older deploys, backups, free disk; per app cubby admin backups # backups by date: database size, sites size, total cubby admin backups create # back up now (waits until done; --no-wait returns at once) cubby admin backups delete 2026-10-01 --yes cubby admin backups keep 3 --yes # keep 3 nightly backups; says what that deletes and frees cubby admin deploys blog # every deploy of an app, * marks the one it serves cubby admin deploys blog prune --keep 2 --yes # delete older deploys, oldest first, sparing the newest 2 cubby admin deploys blog delete <id> --yes
A backup is a pair of files for one day, cubby-<date>.zip (the database) and sites-<date>.tar.gz (sites, jobs and the secrets key). Deleting always removes both. Deleting the newest backup prints a warning, and deleting the only one is refused unless you add --leave-none. Only one backup runs at a time. Storage is measured when you ask and the result is reused for 5 minutes; nothing is stored. Restoring stays on the server on purpose: sudo cubby-server restore <date>. Anyone who isn't an admin gets a clear "needs an admin account" error.
How many backups to keep (1 to 90) is a server setting. BACKUP_KEEP only seeds it on first boot; servers set up before the setting existed keep using BACKUP_KEEP (7 if unset) until you change it. The new count applies at the next nightly backup, which deletes whole pairs by date beyond the newest N. keep tells you first: lowering it lists the dates that will go and the space that frees, and needs --yes; raising it estimates the extra disk from your average backup.
Older deploys are what cubby rollback goes back to (each app keeps its last 10). Deleting them frees their files and jobs and takes them off the app's rollback list. The deploy an app is serving is never deleted; roll back to another one first if you really want it gone.
Scanners and brute force
Each new app gets its own HTTPS certificate, and certificates are published in public logs that vulnerability scanners watch. Soon after you deploy, bots sweep every app for secret files: /.env, /.git/config, /.aws/credentials, /wp-login.php. Cubby never serves those: a path with a part starting with a dot is a plain 404 (except /.well-known/), and none of it counts as page views.
To stop scanners coming back, the installer sets up fail2ban on the host with two jails of Cubby's own:
| Jail | Bans an IP that | For |
|---|---|---|
cubby-probe | asks for scanner paths (dotfiles, *.php, /wp-*, /cgi-bin/, ../ …) 5 times in 10 minutes | for good, until you lift it |
cubby-ratelimit | hits the login/signup limit (10 a minute) 30 times in 10 minutes, so a group signing up together from one network isn't caught | 10 minutes, doubling |
A ban blocks Cubby's web ports only; SSH keeps fail2ban's own sshd jail. Bans go in Docker's DOCKER-USER chain, because ports Docker publishes skip the host's normal firewall chain; scanner bans are kept in an ipset, so thousands of them cost one lookup per connection. Only scanners ask for those paths, so their bans never lift on their own and survive restarts. Most sweeps come from cloud servers whose addresses get reused, though: lift one if it turns out to belong to someone you want in. For fail2ban to see requests, Caddy writes an access log to /var/log/cubby/ (root only) with no headers, cookies or ?token=, kept 7 days.

Admin → Security shows current bans, why, until when, and the hosts and paths each IP swept, with Unban; history is kept 90 days, and a permanent ban stays listed for as long as it lasts. Your agent can do the same with cubby admin bans and cubby admin bans unban <ip> --yes, and every unban is in the activity log. The container never controls fail2ban: it reads ban events from files and asks for an unban with a file that a service on the host checks before acting.
The PocketBase admin UI is only reachable from the server itself: ssh -L 8090:127.0.0.1:8090 you@server, then open http://localhost:8090/_/. If 8090 was taken at install, the installer printed the port it used instead (CUBBY_PB_PORT in /opt/cubby/cubby.env).
Trust
Security model
Cubby is built for groups that know each other. It keeps apps apart and limits the damage of mistakes, but it is not a hostile multi-tenant cloud.
- Apps are isolated. Each app's database calls go through a proxy that only allows that app's collections and file storage. It rejects logins issued by other apps and blocks the database admin UI and settings.
- Only owners change apps. Deploying, changing the schema and deleting all require the owner's token. Admins can delete apps but can't edit them.
- Agents never see passwords. They log in through a link you approve in the browser.
- Deploys are inert files. Uploads are unpacked as plain files only (no symlinks), into folders that can't be escaped. Job code is moved out of the site, so it's never served.
- Jobs are sandboxed. Each run is a separate Node.js process with the permission model on: it can't read the server's files, start processes or see its environment, and it can only connect to public internet addresses, never to the database or anything else on the server's network. Its database client reaches only its own app's records, checked by the same code as the proxy. Runs have a time limit and a 128 MB memory cap, and at most two run at once. This stops accidents and snooping between apps; like the rest of Cubby, it assumes people with accounts aren't attackers.
- Metrics keep no IP addresses. Visitors are counted by a daily-salted hash of IP address and browser, and the salt never leaves memory. IPs are stored only for scanner bans: the access log fail2ban reads (7 days, root only, only while fail2ban is on) and the banned addresses on Admin → Security (90 days).
- Scanners get banned. Requests for secret files are a 404, and fail2ban bans IPs that keep scanning or hammer the login limit, at the firewall.
- Secrets are encrypted with a server key that's kept outside the database (and backed up with it). The database admin UI shows only ciphertext, and values are never shown again after they're set.
- Every agent has its own key. Each approved login gets a separate token, named after its tool and machine. Your manual token is stored so the dashboard can show it again.
cubby agents revokecuts one agent off immediately, and Regenerate replaces the manual token without touching agents. An agent's key can't regenerate the manual token, so revoking an agent really cuts it off, and a login link can be approved only once. - Changes are logged, secrets aren't. The activity log records who changed what and through which agent, for 30 days. It stores secret names only, never values, and like every platform collection it's reachable only through the server, never from apps. Users see their own entries; admins see everyone's.
- Use a domain. On an IP-only server, traffic is unencrypted.
- One outbound request of its own. The daily release check to GitHub is the only request Cubby itself makes, and it sends nothing about your server or users. Admins can turn it off. Apps' scheduled jobs make the requests their owners write.
Answers
FAQ and limits
Can apps have server-side code?
On a schedule, yes: scheduled jobs run code every few minutes, hours or days, with their own app's data and secrets. Code that runs per request (API endpoints, hooks) isn't supported; browsers talk to the database directly, under its rules. That covers most group tools: polls, trackers, lists, boards, small games.
Can people outside the server use an app?
Yes. Apps are public at their URLs, and each app has its own sign-up. Only the dashboard and gallery need a Cubby account.
How big can it get?
All apps share one SQLite database, which comfortably serves a group of friends or a small team. One very busy app slows the others. There's no per-app data quota yet; the Admin page shows disk use so you can clean up.
Which agents work?
Any agent that supports skills (Claude Code, Codex, Cursor, OpenCode). Others can still use Cubby by reading <server>/docs.md and running the CLI.
What's not supported yet?
Server code that runs per request (API endpoints, hooks), custom domains per app, single sign-on across apps, and app emails (verification, password reset). Jobs can send email through a provider's API with your own key.
Can I use several Cubby servers?
Yes. Paste each server's setup prompt into your agent once; the first one becomes the default for new apps; your agent then picks a server by its name, and asks if it isn't sure. cubby servers lists them and cubby use sets the default. Servers on different Cubby versions work side by side: the CLI always matches the server it's talking to (in an app folder, the server in cubby.json), downloading that server's version of the CLI once. Agents find out what a server supports with cubby help --json. Servers older than Cubby 0.1.1 aren't supported; the CLI tells you to ask the admin to run sudo cubby-server upgrade.
A server moved to a new address. How do I move my app?
Each app's cubby.json remembers its server's address. Log in to the new address, then run cubby link <app> --server <new-url> in the app's folder.
Which version am I on, and how do I update?
The Admin page shows the version at the bottom and tells you when a newer release is out. On the server, sudo cubby-server version shows the details and sudo cubby-server upgrade updates (it backs up first). Normal releases go 0.1.1 → 0.1.2; only a major change moves to 0.2.0.
My apps got hundreds of hits on /.env and /.git/config. Am I hacked?
No. That's a vulnerability scanner: it finds new apps through public certificate logs and asks every one for secret files. Cubby answers those with a 404, doesn't count them as views, and fail2ban bans the scanner's IP after 5 tries. Check Admin → Security; on servers from before the bans existed, run sudo cubby-server fail2ban enable once. A scan spread over many IPs still gets a few 404s through from each, and IPv6 visitors can't be banned, because they reach Cubby through Docker's proxy.
My agent's login link expired.
Links last 10 minutes. Ask the agent to try again; it starts a fresh one.
Contribute
Develop Cubby
git clone https://github.com/chaitanyadukkipaty/cubby && cd cubby cp .env.example .env npm run setup # downloads PocketBase into ./bin, installs deps npm run dev # http://localhost:8080, apps at http://<app>.localhost:8080 npm test # unit, integration and browser tests
The repo has three parts: server/ (control API, hosting, database proxy), cli/ (the CLI, app template and agent skill) and docker/ + install.sh (packaging). The main branch is protected: every change lands through a pull request with passing tests. Before opening one, read the contribution guide: branch naming, Conventional Commits, tests at every level of the pyramid, and docs in the same PR.