cubby.

A home for the apps your agents build.

Cubby is a server you host once and share with friends or colleagues. Everyone asks their AI coding agent for an app, and Cubby gives it a real database, user accounts and a live URL. Everyone can open everyone's apps; only you can change yours.

Friend Polls, an app built by an AI agent and hosted on Cubby, showing two polls with live vote results
Friend Polls: built and deployed by an agent from one sentence, in about three minutes.
  1. adminRuns one install command on a Linux server.
  2. friendsSign up with the invite code and connect their agent.
  3. you“Build me a polls app and host it on Cubby.”
  4. agentBuilds it, deploys it, and hands you the URL.

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.

CubbyHosted buildersA VPS by hand
Who owns the server and dataYouThe platformYou
Use your own agent (Claude Code, Codex, Cursor…)YesTheir agent onlyYes
Database, logins, file uploads, realtime per appBuilt inBuilt inYou set it up per app
Deploy is one command the agent runs itselfYesYesNo
Friends get their own accounts and appsYesSeparate accountsShared SSH, or nothing
CostOne small VPSPer-seat plansOne 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:

QuestionWhat to answer
Domaine.g. cubby.example.com, or leave blank to use the server's IP address
Admin emailYour email. You'll log in to the dashboard with it.
Admin passwordAt least 8 characters. It isn't stored on disk after setup.
Name your cubbyWhat 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?

ModeApp URLsHTTPS
Domain (recommended)https://polls.cubby.example.comAutomatic, free certificates per app
IP onlyhttp://polls.203-0-113-5.sslip.ioNo. 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.

Cubby dashboard Home for a new user: step 1 with the Copy prompt for agent button, step 2 approve the login, step 3 prompts to ask for an app
Home, what a friend sees after signing up.

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.

Approval page saying Claude Code on priya-mbp wants access, with a matching code and an Approve button
The approval page your agent links you to.

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.

The Agents page: Claude Code and Codex with their machine, the apps each touched, last seen, and Rename and Revoke buttons, above the token for scripts and CI
The Agents page.
cubby agents rename p3n7w9s2t5u8v1y "Old desktop"
cubby agents revoke p3n7w9s2t5u8v1y --yes   # refuses without --yes

An 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.

Dashboard Home: prompt suggestions, a failed job with Copy fix-it prompt, and the agent activity timeline with Roll back on deploys, next to active visitors, 7-day views and agents
Home: what your agents did, and what to ask next.

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:

  1. Picks a name, then runs cubby init friend-polls to scaffold Vite, React, Tailwind and the database client.
  2. Designs the data model and its security rules in cubby/schema.json.
  3. Writes the UI, then ships with cubby deploy: schema, build and upload in one step.
  4. 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.

The Explore page listing everyone's apps: Team Tasks by Sam, Friend Polls and Book Club by Priya, Recipe Box by Arjun
Explore: what friends on the server have shipped.
Changing an app later. The server stores built output, not source code. Keep working in the folder the app was built in, and ask your agent for changes there.

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 deploy bundles the jobs with the frontend, and cubby rollback brings back the jobs of that deploy.
  • Its own data, past the rules. A job's pb acts 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 as env.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.
Friend Polls' Jobs tab: two scheduled jobs that ran OK, each with its schedule, last and next run, and Run now and Pause buttons
An app's scheduled jobs, on its Jobs tab.

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 /pb counts 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 1h after 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 30d are reported by the hour; longer ones (up to 1y) by the day.
  • Yours to see. Only the app's owner (and the server's admins) can see an app's numbers. cubby metrics without an app lists all of yours; --range takes 1h, 24h, 7d, 30d, 1y or any hours or days in between, and --json adds 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, from cubby metrics --all.
Friend Polls' Overview tab: a KPI strip, then views, unique visitors, active visitors and API errors charted on one time axis with dashed deploy lines, and the top paths
An app's Overview: deploys v3 and v4 drawn through every chart.

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. --drop removes the collection too, and the next cubby deploy pushes schema.json again.
  • 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,b deletes only those, and only they go in the snapshot. --dry-run shows "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 --yes shows 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 in cubby 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.
Friend Polls' Data tab: collections with record counts and file sizes (users unticked), Empty, Drop or delete by filter, a preview that says Empties comments, polls and votes: 4,231 records with five sample records, the app name typed to confirm, and two snapshots with Restore and Purge
An app's Data tab, after Preview.

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 --yes replaces 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-run runs 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. --wipe drops every collection, and the app's next cubby deploy recreates them from schema.json.
  • What comes back different. PocketBase sets created and updated itself 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.

CommandWhat 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 serversList the servers you've added, with the default marked.
cubby use <name|url>Make a server the default for new apps.
cubby whoamiShow the account and server you're using.
cubby agentsList 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> --yesRevoke 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 versionShow 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 deploysList 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 jobsList 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 --yesSecrets 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 | pullApply or download the data model. Push never deletes anything.
cubby schema drop <c>[.<f>] --yesDelete 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] --yesEmpty 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] --yesDelete 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 --yesDrop every collection of the app (after a snapshot); the next deploy pushes the schema again.
cubby data purge <id> --yesDelete 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> --yesDelete an app, its data and deploys.
cubby admin storageAdmins 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>] --yesAdmins 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 | statusManage 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.

Admin Overview: users, views, visitors, active now, API error rate and free disk across the server, a 7-day views chart of every app, the top apps by views, the most active agents across users, the disk bar and an update notice
Admin → Overview.

On the server

CommandWhat it does
cubby-server status · logs · restartCheck on it
cubby-server upgradeBack 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 versionWhat's running, what it follows, and the latest release
cubby-server codeShow the current signup code
cubby-server backup · backupsBack 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 · disableInstall 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.
Admin Storage: a disk bar split into database, uploads, current and old deploys, backups, snapshots, other files and free space; the nightly backups with Back up now, a copyable restore command and Delete on each; apps by size; and data-clear snapshots with Purge
Admin → Storage.

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:

JailBans an IP thatFor
cubby-probeasks for scanner paths (dotfiles, *.php, /wp-*, /cgi-bin/, ../ …) 5 times in 10 minutesfor good, until you lift it
cubby-ratelimithits the login/signup limit (10 a minute) 30 times in 10 minutes, so a group signing up together from one network isn't caught10 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: IPs banned now with why (scanning for secret files, too many login attempts), when, when the ban lifts and an Unban button; a scanner that swept six apps with the ten paths it asked for; and the last 90 days of bans
Admin → Security.

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 revoke cuts 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.