Actions
16 actions, callable from the panel, the command palette and the API as
POST /api/v1/a/<id>. Internal actions used between modules are not listed.
| Action | What it does | Risk | Preview |
|---|---|---|---|
projects.list | List projects (read) | low | No dry run |
projects.show | Show a project (read) | low | No dry run |
projects.logs | Logs of a project (read) | low | No dry run |
projects.create | Create a project | medium | |
projects.update | Change a project | low | |
projects.add | Add to a project | low | |
projects.remove | Take out of a project | low | |
projects.detect | Find what uses a folder (read) | low | No dry run |
projects.delete | Delete a project | high | |
projects.deploy | Deploy a project | medium | |
projects.deploy.list | Project deploys (read) | low | No dry run |
projects.deploy.get | One project deploy with its log (read) | low | No dry run |
projects.rollback | Roll a project deploy back | high | |
projects.export | Export a project | medium | |
projects.import | Import a project | high | |
projects.docs.write | Rewrite PROJECT.md | low |
Permissions and limits
Permissions
projects.viewView projects, their health, deploys and logsprojects.manageCreate, change and delete projectsprojects.deployDeploy a project and roll it backprojects.exportExport a project to one fileprojects.importImport a project from a file
Engineering notes
Generated from modules/projects/docs.md at build b69cc03e. These are the notes the engineers keep
next to the code: precise, technical, and honest about what is not done yet.
Spec: docs/specs/projects.md. Decision: docs/adr/0035-projects-hold-references.md. Control plane cp/, file format
bundle/ (pure Go, tested), agent ops internal/agent/projects.go (types sdk/agentop/projects.go), UI ui/.
Model
projects(account, name, slug unique per account, main domain, root folder relative to the home, description, last health for the list, the PROJECT.md state),project_items(kind,ref_id, last known label, deploy position,gone_at;UNIQUE (kind, ref_id): one thing is in one project),project_deploys(steps, one log; one running per project),project_events(the activity list).- References only. Every read goes through the owning module's action as the person (
web.sites.get,apps.app.get+apps.status,databases.db.get,databases.valkey.list,accounts.cron.list,ssl.cert.get,mail.domain.get,backups.job.list,domains.get), so the module's own permissions, scoped grants and denies apply.not_found= gone (marked once, with an activity line),forbidden= hidden, anything else = could not check. Adding checks the same way and refuses another account's resource or one already in a project. - The kinds that have no get action (valkey, cron, backup jobs) are read from the account's list once per request.
Health and fixes (kinds.go)
One problem per item, the worst first; each with one fix that is a real action the page runs as the person (with its
preview when the action has one): deploy now/again, start a stopped process (apps.unit.control), get a certificate,
renew, verify a domain, back up now, apply a website again (administrators), take a gone item out, rewrite PROJECT.md.
The page adds "Ask the assistant" to every problem. Health of the project = its worst item (gone counts as a warning).
A 15-minute schedule (projects.refresh) keeps the list's health current; the page always reads live.
Deploy (deploy.go)
Apps in their order (projects.update {deploy_order}), each with apps.deploy.run {wait: true}; each app's deploy
log goes into the one job log (<app> | <line>) and the stored record. A failure stops the run and rolls the apps
already deployed in this run back to their release before (apps.deploy.rollback {release}); an app without one stays.
projects.rollback reverses a finished run the same way. The job fails when the deploy did (the record keeps the log).
Export and import (bundle.go, bundle/, internal/agent/projects.go)
project.json(respirecloud/project/v1): the project and each resource's definition, written from the typed rows the modules return (an allow-list, never their raw objects): no secret value ever leaves (app secrets keep their names, database and Valkey passwords are not exported).- Data: database dumps through
databases.db.export(the dump code backups uses) into a staging folder in the home, Valkey data files as last saved (~/valkey/<name>/, not for persistencenone), the project folder (optionally withoutnode_modules/.venv). Data needs a passphrase (10+ characters). - The file is packed and opened on the node by a helper running as the account's Unix user (like the file manager),
with the file manager's walk and extractor: links and special files are never packed or extracted,
..and absolute names are refused, size/count/ratio limits (64 GB, 2 million files). Format:bundle/bundle.go(age-style STREAM, scrypt + ChaCha20-Poly1305; a wrong passphrase, a changed byte, a cut file or extra bytes are refused). - Import: open (manifest only) -> check what already exists (a conflict stops it before anything is made; an
existing domain of the same account is reused) -> domains (
skip_autoweb, mail) -> unpack -> databases (+users with new passwords in the vault, +databases.db.import) -> Valkey (its files are in place first) -> websites -> apps (non-secret variables; the website in front of an app comes withapps.app.create {domain_id}) -> cron (old home and folder paths rewritten) -> mail and a backup job on the default target (a failure there is a to-do, not an undo) -> the project, its items and PROJECT.md. Any essential failure undoes everything, newest first, through the modules. The result lists what to do by hand (secret values, deploy keys, DNS, deploy).
PROJECT.md (docs.go)
A section between <!-- respirecloud:project:begin --> and <!-- respirecloud:project:end --> in the project folder,
written through files.read/files.write (as the person; as the system from the event handler, declared in
system_calls). Rewritten on every change of the project, after a deploy, and on the events web.site.updated/deleted,
apps.app.deleted, apps.deploy.finished, databases.db.deleted, domains.deleted, mail.domain.removed. Text
outside the markers is kept; an existing CLAUDE.md without PROJECT.md gets the section under its first heading.
Only written when the text changed. A failure is shown on the page with "Write it again".
Make a project from a folder (show.go, projects.detect)
Websites whose document root is in the folder; apps that name a path in it anywhere in their definition (working
folder, checkout, document root: robust to new app fields) or whose website matched; cron lines that name it; the
domains and certificates of those websites; databases and Valkey named in the folder's .env or the apps' non-secret
variables (DB_DATABASE, DATABASE_URL, REDIS_PORT, REDIS_URL ...); mail for the main domain. Each with why, and
whether it is already in a project.
CLI
rcctl projects list | show | create | add | remove | detect | deploy | rollback | export | import | update | delete
(action ids projects.*). rcctl projects show shop, rcctl projects deploy shop, rcctl projects export shop --include-data --passphrase ..., rcctl projects import --path project-exports/shop-....rcproject --passphrase ....
A project is named by its id or, in one's own account, its name.
Tests
go test ./modules/projects/... with scripts/test-all.sh (throwaway Postgres): a stand-in of the other modules
answers the real host calls (cp/world_test.go); create/add/show with health, gone + fixes + the event, deploy order
and rollback, export/delete/import with data and its undo, detect, access. internal/agent TestProjects*: pack and
open as files on disk, escapes, wrong passphrase. Lab: lab/tests/projects.sh (CLI noun, two labs: export on one,
import on a fresh one) and web/apps/panel/scripts/lab-walk-projects.mjs (1440/360 px, light/dark).
Not done / wished from other modules
- A singular CLI noun
rcctl project ...needs an alias ininternal/cli(H1); today it isrcctl projects .... databases.valkey.get(a get by id) anddatabases.valkey.export/import(a consistent dump instead of the files as last saved);accounts.cron.get;backups.job.get.apps.env.export(secret values, step-up, for encrypted exports) so an import needs no re-entry of secrets.- A git-connection kind once
modules/git(card N) lands: one row inkinds.go. - Pack and open run in one agent op each, capped at 30 minutes by the host; a very large project folder needs more.
Login users, import and delete (fix batch, 2026-10-27)
- Deleting a database through a project (and rolling back an import) also deletes the login users whose only access was that database, so an export, delete and import round trip works in the same account.
- Import: a login user already in the account with no database is used as it is (given access to the new database, keeping its password); one that belongs to another database is a conflict listed before anything is made.
- A database create that fails on its user removes the database it made.