Basecamp · alpha
Basecamp is where you go when you stop building one app and start operating several. Ship a release, roll it back, see what is running where, and get your data back when something breaks — on boxes you own, with no one in the middle.
Deploy pipeline
A deploy becomes a release: built, loaded onto your machines, started, health-checked and written down. If the new container will not come up, the old one keeps serving.
fli deploy, press Deploy on the App's page, or push to the branch it follows. Each is recorded with who shipped it, the commit and the image digest. From your laptop the image streams over ssh with docker save | docker load, so you need no registry. packages/cli/commands/basecamp/deploy.md. The step names are the ones buildInitialSteps in deployments.service.ts writes, and the ones the release page lists. A Deploy press on a git App runs Build image on the machine instead, and an image App runs Pull image. The swap and put-back are packages/outpost/src/docker.js. Read more: fli basecamp:deploy, releases and rollback, Outpost, the deploy hazards.
Outpost agent
Outpost is the one process Basecamp installs on each server. It does what a signed command asks and reports back on a timer, so the box keeps serving while Basecamp is down.
_replacedStart the new oneHealth-check for up to 30 secondsPOST /deploy does on the box, in order: packages/outpost/src/server.js and the swap in src/docker.js. The old container is stopped first because a SQLite file takes one writer. Read more: Outpost and its protocol · what bites on a box · backups and live replicas
Auth and trust
Who may read and change what is declared in the schema and enforced where the data is read, not left to each code path to remember. It rests on one value you keep yourself: lose ENCRYPTION_KEY and every encrypted column, in every backup taken under it, cannot be read back.
tenancy line scopes every workspace-owned row, and your role is read per request from your membership in the workspace you are addressing, so the same person can own one workspace and only view the next. servers:read, projects:write), can never mint another key, and is kept only as an HMAC, so it is shown once and refused on its next request after you revoke it. // packages/basecamp/db/schema.lite, abridged
tenancy { strategy row column workspaceId claim workspaceId }
model Invitation {
token String @unique(global) @secret @hashed
@@gate(5)
}
model Secret {
data String @default("{}") @encrypted
@@gate(5)
@@trail(audit)
}
model ApiKey {
userId String @immutable @default(auth().id)
credentialId String? @system
scopes String[] @default([]) @immutable
revokedAt DateTime? @system
@@gate(5)
@@trail(audit)
}
model Server {
enrollTokenHash String? @guarded
enrollExpiresAt DateTime?
outpostPublicKey String?
outpostCert String?
@@gate(2.4.4.5)
}packages/basecamp/db/schema.lite. @@gate(5) means an admin or owner of the workspace; 2.4.4.5 is the role each of read, create, update and delete needs. @encrypted, @guarded and @hashed are enforced by Litestone at the data layer, and the trail logs them as [redacted]. Read more: the schema and its access ladder · enrollment and signed commands · how fields are encrypted · keeping the encryption key
Monitoring and alerting
Basecamp asks your URLs whether they are up, notices when a box stops checking in, and grades the numbers you set rules on. When something changes, the right person hears about it once, on the channel they chose.
200-299,301 by default), checked every two minutes from the Basecamp process. It goes down only after its retries fail in a row, so one 502 behind a load balancer pages nobody, and a monitor can re-page every N checks while it stays down. model Monitor {
name String @trim @length(1, 200)
url String @trim @url
method HttpMethod @default(GET)
intervalSeconds Int @default(120) @gte(60) @lte(86400)
retries Int @default(2) @gte(0) @lte(10)
retryIntervalSeconds Int @default(120) @gte(60)
timeoutMs Int @default(10000) @gte(1000) @lte(60000)
acceptedStatus String @default("200-299") @trim @length(3, 200)
isInverted Boolean @default(false)
resendEvery Int @default(0) @gte(0)
// …
}
model AlertRule {
name String @trim @length(1, 200)
severity AlertSeverity @default(warning)
metricName String @trim @length(1, 200)
operator ComparisonOp @default(gt)
threshold Float
forMinutes Int @default(0) @gte(0) @lte(1440)
isActive Boolean @default(true)
snoozedUntil DateTime? @system
// …
}packages/basecamp/db/schema.lite, models Monitor and AlertRule (comments and relations trimmed). Grading is domain/monitor/probe.ts and jobs/alert-evaluate.job.ts. Read more: Basecamp layout · every job and its schedule · every notification and its transports · Outpost and its heartbeat
Backups and recovery
Basecamp archives itself and every app it runs into a bucket you own, prunes the pile for you, and restores a hub from the setup page of a fresh install. You can run that restore on a scratch box today, before the day you need it.
VACUUM INTO so a write in flight cannot tear it. An app's archive is its data volume, tarred by the Outpost on its machine, on a cron per app and before any release of an app that has a volume. ENCRYPTION_KEY, /setup lists the archives in your bucket. Pick one and it is checked before a file is touched: a wrong key, a damaged file or a schema this version cannot carry on from is refused with its reason. Restart, and the old hub's owner signs in. The key is not in the archive, so Basecamp grades the hub red until you attest that you kept a copy off the box. bun run verify:hub-restore drives in a browser: a hub backed up to a SeaweedFS bucket, restored at /setup across a restart, then its app and its own replicas found. From packages/basecamp/web/test/verify-hub-restore.mjs and docs/backups.md § 7. Proved against a local S3 stand-in; a live run on a real bucket is still owed. Read more: the backups recipe, what a drill proves, the restore drive.
Start
Name a hostname. fli basecamp:create buys the server, installs Basecamp, points DNS at it and makes your first admin.
Basecamp is alpha and built in FrontierJS itself. It is a face over a documented path: everything it does is also available from fli and, beneath that, ssh.