Overview

The systemcontroller is the central backend service for Town OS. It is built on Echo v5 and listens on port 5309 (TCP) or a Unix domain socket in production. All request and response bodies use JSON. Errors follow RFC 9457 (application/problem+json).

CORS is enabled for development. In production the API is served behind the same origin as the UI.

Authentication

Authenticate by calling POST /account/authenticate with a username and password. The response contains a Bearer token. Include it in subsequent requests:

Authorization: Bearer <token>

Sessions expire after 7 days of inactivity. There are three auth levels:

LevelDescription
PublicNo token required.
AuthenticatedAny valid session token.
AdminSession token belonging to an admin account.

Pagination

All list endpoints accept the following query parameters and return a common envelope:

ParameterTypeDescription
sort_bystringField name to sort on.
sort_orderstringasc or desc.
limitintPage size (default 20).
offsetintPagination offset.
searchstringCase-insensitive substring match across all string fields.

Response Envelope

{
  "entries":     [...],
  "has_more":    true,
  "total_pages": 5,
  "total_count": 97
}

Status

GET /status/ping Public

Health check and system overview. Unauthenticated callers receive a minimal response with status and needs_setup. Authenticated callers receive the full dashboard payload including filesystem count, package counts, unit status summary, disk usage, external/internal IP, and upgrade availability.

Accounts

POST /account/authenticate Public

Authenticate with username and password. Returns a session token and the account object.

FieldTypeDescription
usernamestringRequired. Account username.
passwordstringRequired. Account password.
POST /account/create Public

Create a new account. The first account created becomes the administrator. Password must be at least 8 characters. Email, phone, and real name are required.

FieldTypeDescription
usernamestringRequired.
passwordstringRequired. Minimum 8 characters.
emailstringRequired.
phonestringRequired.
real_namestringRequired.
adminbooleanWhether the account has admin privileges.
POST /account Authenticated

Get a single account by username. Request body: {"username": "alice"}.

GET /account Authenticated

List all accounts. Supports pagination parameters.

POST /account/update Authenticated

Update account fields. Send username to identify the account and a fields object with any combination of password, email, phone, real_name, and admin. Only provided fields are changed.

GET /account/me Authenticated

Returns the username associated with the token in the Authorization header.

GET /account/sessions Authenticated

List all active sessions for the authenticated user. Each session includes its ID, username, creation time, and last-used time.

POST /account/session/revoke Authenticated

Revoke a session by ID. Request body: {"session_id": "..."}.

POST /account/disable Admin

Disable an account. Request body: {"username": "bob"}.

POST /account/enable Admin

Re-enable a disabled account. Request body: {"username": "bob"}.

Storage

POST /storage Authenticated

List filesystems. Accepts pagination parameters plus optional name (prefix filter) and state (user, installed, or uninstalled) in the request body.

POST /storage/create Authenticated

Create a new btrfs subvolume. Send name and optional quota (bytes). If quota is 0 or omitted, the system default (50 GB) is used. Reserved names (installed, uninstalled, archives) are rejected.

POST /storage/modify Authenticated

Modify an existing filesystem. Send name to identify it and a filesystem object with the updated name and/or quota.

POST /storage/remove Authenticated

Remove a filesystem. Request body: {"name": "mydata"}.

POST /storage/upload-archive Admin

Upload and unpack an archive into a target subvolume. Accepts multipart/form-data with a subvolume field and an archive file. Supports .tar.gz, .tgz, .tar.bz2, .tbz2, .tar.xz, .txz, .tar, .zip, and .7z.

SettingDefaultDescription
max_archive_size20 MBMaximum upload size.
archive_unpack_timeout120 secondsMaximum time for unpacking.
POST /storage/download-archive Admin

Download a 7z archive of subvolume contents. Send subvolume (required), optional paths (string array for specific files), and optional stop_service (systemd unit to stop during archiving). Returns a binary stream.

Repositories

GET /repository Authenticated

List all configured package repositories with name, URL, and any error status. Supports pagination parameters.

POST /repository/add Authenticated

Add a new package repository. Triggers an immediate refresh.

FieldTypeDescription
namestringRequired. Display name for the repository.
urlstringRequired. Git URL of the repository.
usernamestringOptional. Auth username for private repos.
passwordstringOptional. Auth password for private repos.
POST /repository/remove Authenticated

Remove a repository by name. Triggers an immediate refresh. Request body: {"name": "my-repo"}.

POST /repository/move Authenticated

Reorder a repository to a new zero-based position. Later repositories override earlier ones when package names collide. Request body: {"name": "my-repo", "position": 0}.

POST /repository/refresh Authenticated

Force an immediate refresh of all repository metadata. Returns an empty body on success, or a JSON object mapping repository names to error strings if any fail.

Packages

GET /packages Authenticated

List all available packages across all repositories. Each entry includes repo, name, version, description, supplies tags, installation status, and whether an upgrade is available. Supports pagination parameters.

GET /packages/by-repo Authenticated

List packages grouped by repository. Accepts an optional search query parameter. Returns an array of {"repo": "...", "packages": [...]} groups.

GET /packages/timezones Authenticated

List all available IANA timezone names for use in package configuration.

GET /packages/installed Authenticated

List installed package identifiers. Supports pagination parameters.

POST /packages/installed/info Authenticated

Get detailed info for an installed package. Send repo, name, and version. Returns questions, user responses, notes, and note types.

POST /packages/responses Authenticated

Get the saved question responses for an installed package. Send repo, name, and version. Returns a key-value map of responses.

POST /packages/versions Authenticated

List available versions for a package. Request body: {"name": "nginx"}. Returns a string array of version identifiers.

POST /packages/children Authenticated

List child packages. Send repo and name. Returns a string array.

POST /packages/questions Admin

Get the installation questions for a package. Request body: {"name": "nginx"}. Returns a map of question key to {"query": "...", "type": "..."}.

POST /packages/questions/identity Admin

Get questions for a specific package version. Send repo, name, and version.

POST /packages/install-preview Admin

Preview what an installation will do before committing. Send repo, name, and version. Returns volume details, port mappings, disk usage, quota information, upgrade source version, and a human-readable summary.

POST /packages/install Admin

Install a package.

FieldTypeDescription
repostringRequired. Repository name.
namestringRequired. Package name.
versionstringRequired. Version to install.
responsesobjectRequired. Key-value answers to installation questions.
reuse_volumesbooleanReuse existing data volumes from a previous installation.
import_from_versionstringVersion to import volumes from during upgrade.
POST /packages/uninstall Admin

Uninstall a package. Send repo, name, version, and optional purge_volumes (boolean) to delete associated data.

POST /packages/disable Admin

Disable an installed package (stop its service). Send repo and name.

POST /packages/enable Admin

Re-enable a disabled package (start its service). Send repo and name.

POST /packages/purge-volumes Admin

Delete all data volumes for an installed package. Send repo and name.

POST /packages/uninstalled-volumes Admin

Check whether a package has leftover volumes from a previous installation. Send repo and name. Returns has_uninstalled_volumes, uninstalled_versions, and installed_versions.

POST /packages/purge-uninstalled-volumes Admin

Delete leftover volumes from previously uninstalled versions. Send repo and name.

GET /packages/upgrades Authenticated

List available upgrades for installed packages. Each entry includes installed_version, latest_version, and whether the package definition has changed.

POST /packages/upgrades/dismiss Admin

Dismiss the current upgrade notifications. Send an empty JSON object.

Systemd

GET /systemd/units Authenticated

List systemd units managed by Town OS. Each entry includes unit name, description, load/active/sub states, the associated package identifier and description, and a failure flag. Supports pagination parameters.

POST /systemd/status Admin

Control a systemd unit. Send name (unit name) and action (start, stop, or restart).

GET /systemd/logs Authenticated

Stream journal entries for a unit in real time via Server-Sent Events. Pass the unit query parameter. Each SSE event contains a JSON-encoded journal entry with fields like Message, Priority, RealtimeTimestamp, and SystemdUnit.

GET /systemd/logs/tail Authenticated

Fetch a page of journal entries with cursor-based pagination and filtering.

ParameterTypeDescription
unitstringRequired. Systemd unit name.
linesintRequired. Number of entries to return.
beforestringCursor — return entries before this position.
afterstringCursor — return entries after this position.
grepstringCase-insensitive substring filter on message text.
sinceintUnix timestamp — return entries from this time forward.
untilintUnix timestamp — stop collecting at this time.

Returns entries, cursor (first entry), and end_cursor (last entry) for subsequent pagination.

Settings

GET /settings Admin

Get all settings as a key-value object.

POST /settings/get Admin

Get a single setting. Request body: {"key": "default_quota"}. Returns key and value.

POST /settings/set Admin

Set a setting value. Request body: {"key": "default_quota", "value": "107374182400"}.

Default Settings

KeyDefaultDescription
default_quota53687091200 (50 GB)Default quota for new filesystems.
max_archive_size20971520 (20 MB)Maximum archive upload size.
archive_unpack_timeout120 (seconds)Maximum time for archive unpacking.

Audit Log

POST /audit/log Admin

List audit log entries. All fields in the request body are optional.

FieldTypeDescription
before_idintKeyset pagination — return entries with ID less than this.
accountstringFilter by account username.
sort_bystringField to sort on.
sort_orderstringasc or desc.
limitintPage size.
offsetintPagination offset.
searchstringSearch filter.

Each audit entry contains id, account, action, path, detail, success, error, and created_at. Audited actions include: authenticate, create/update/disable account, revoke session, install/uninstall/disable/enable package, create/modify/remove filesystem, add/remove/move/refresh repository, upload/download archive, update setting, dismiss upgrades, and purge volumes.

Client Libraries

Town OS ships with Go and JavaScript client libraries that provide full API coverage. Both clients throw typed errors on non-200 responses using RFC 9457 problem detail.

Go Client

The Go client lives in src/svc/systemcontroller/client.go and implements the Client interface. It supports both Unix socket and HTTP connections.

// Connect via Unix domain socket (production)
client := systemcontroller.InitClient("/run/town-os/systemcontroller.sock")

// Connect via HTTP (development / testing)
client := systemcontroller.FromClient(http.DefaultClient, "http://localhost:5309")

Set client.Token after authenticating. All methods accept a context.Context as their first parameter.

Storage

MethodDescription
CreateFilesystem(ctx, fs)Create a new btrfs subvolume.
ModifyFilesystem(ctx, name, fs)Rename or resize a filesystem.
RemoveFilesystem(ctx, name)Delete a filesystem by name.
ListFilesystems(ctx, prefix, state, params)Paginated list filtered by name prefix and state ("user", "installed", "uninstalled").

Repositories

MethodDescription
AddRepository(ctx, name, rawURL, username, password)Register a package repository with optional credentials.
RemoveRepository(ctx, name)Remove a repository by name.
MoveRepository(ctx, name, position)Change priority (0 = highest).
RefreshRepositories(ctx)Refresh all metadata. Returns map of errors.
ListRepositories(ctx, params)Paginated list of repositories.

Packages

MethodDescription
ListPackages(ctx, params)Paginated list of available packages.
ListPackagesByRepo(ctx, params)Packages grouped by repository.
ListPackageVersions(ctx, name)Available versions of a package.
GetPackageQuestions(ctx, name)Configuration questions by name.
GetPackageQuestionsByIdentity(ctx, repo, name, version)Questions for a specific version.
ListChildren(ctx, repo, name)Child package names.
InstallPreview(ctx, repo, name, version)Preview volumes and ports without installing.
InstallPackage(ctx, name, version, responses, reuseVolumes, importFromVersion, skipResponseReuse)Install a package. Name uses "repo/package" format.
UninstallPackage(ctx, repo, name, version, purgeVolumes)Remove an installed package.
DisablePackage(ctx, repo, name)Stop services without uninstalling.
EnablePackage(ctx, repo, name)Re-enable a disabled package.
PurgeVolumes(ctx, repo, name)Delete all data volumes for a package.
ListUninstalledVolumes(ctx, repo, name)Check for leftover volumes.
PurgeUninstalledVolumes(ctx, repo, name)Delete leftover volumes.
ListInstalled(ctx, params)Installed packages as "repo/name@version".
GetResponses(ctx, repo, name, version)Stored configuration responses.
GetInstalledInfo(ctx, repo, name, version)Detailed info including questions, responses, and notes.
ListTimezones(ctx)Available IANA timezone names.

Systemd

MethodDescription
ListUnits(ctx, params)Paginated list of systemd units.
SetUnitStatus(ctx, name, action)Apply "start", "stop", or "restart".
LogReplay(ctx, name)Stream journal entries via SSE. Returns a channel.
LogTail(ctx, params)Page of journal entries with cursor-based pagination, grep, time range, and priority filtering.

Accounts

MethodDescription
Authenticate(ctx, username, password)Returns session token and account.
CreateAccount(ctx, username, password, email, phone, realName, admin)Create a user. Password minimum 8 characters.
GetAccount(ctx, username)Retrieve account by username.
UpdateAccount(ctx, username, fields)Modify account fields (password, email, phone, real_name, admin).
ListAccounts(ctx, params)Paginated list of accounts.
DisableAccount(ctx, username)Prevent authentication.
EnableAccount(ctx, username)Re-enable a disabled account.
ListSessions(ctx, token)Active sessions for the token's user.
SessionUsername(ctx, token)Username for a session token.
RevokeSession(ctx, sessionID)Invalidate a session.

Audit, Settings & Upgrades

MethodDescription
ListAuditLog(ctx, opts, token)Paginated audit log with filters.
GetSettings(ctx)All settings as key-value map.
GetSetting(ctx, key)Single setting by key.
SetSetting(ctx, key, value)Update a setting.
ListUpgrades(ctx)Packages with newer versions available.
DismissUpgrades(ctx)Mark pending upgrades as dismissed.

Archives

MethodDescription
UploadArchive(ctx, subvolume, archiveReader, filename, subpath, stopService)Upload and extract an archive into a subvolume. Formats: tar.gz, tar.bz2, tar.xz.
DownloadArchive(ctx, subvolume, paths, stopService, format)Create an archive of subvolume contents. Returns an io.ReadCloser.

Health

MethodDescription
Ping(ctx)Service health and summary counts.

JavaScript Client

The JavaScript client lives in ui/src/api/ and is used by the Town OS dashboard UI. It is built as a modular set of mixins on the SystemControllerClient class. Non-200 responses throw ApiError with the parsed RFC 9457 problem detail.

import SystemControllerClient from './api/client.js';

const client = new SystemControllerClient('http://localhost:5309');

// After authentication
const result = await client.authenticate('admin', 'password');
client.setToken(result.token);

Storage

MethodDescription
createFilesystem(fs)Create a new btrfs subvolume.
modifyFilesystem(name, fs)Rename or resize a filesystem.
removeFilesystem(name)Delete a filesystem by name.
listFilesystems(prefix, sortBy, sortOrder, state, limit, offset, search)Paginated list with filtering.

Repositories

MethodDescription
addRepository(name, url, username?, password?)Register a repository with optional credentials.
removeRepository(name)Remove a repository by name.
moveRepository(name, position)Change priority (0 = highest).
refreshRepositories()Refresh all metadata. Returns error map or null.
listRepositories(sortBy, sortOrder, limit, offset, search)Paginated list.

Packages

MethodDescription
listPackages(sortBy, sortOrder, limit, offset, search)Paginated list of available packages.
listPackagesByRepo(search)Packages grouped by repository.
listPackageVersions(name)Available versions of a package.
getPackageQuestions(name)Configuration questions by name.
getPackageQuestionsByIdentity(repo, name, version)Questions for a specific version.
installPreview(repo, name, version)Preview volumes and ports without installing.
installPackage(repo, name, version, responses, reuseVolumes?, importFromVersion?)Install a package with configuration answers.
uninstallPackage(repo, name, version, purgeVolumes?)Remove an installed package.
disablePackage(repo, name)Stop services without uninstalling.
enablePackage(repo, name)Re-enable a disabled package.
purgeVolumes(repo, name)Delete all data volumes for a package.
listUninstalledVolumes(repo, name)Check for leftover volumes.
purgeUninstalledVolumes(repo, name)Delete leftover volumes.
listInstalled(sortBy, sortOrder, limit, offset, search)Installed packages as "repo/name@version".
getResponses(repo, name, version)Stored configuration responses.
getInstalledInfo(repo, name, version)Detailed info including questions, responses, and notes.

Systemd

MethodDescription
listUnits(sortBy, sortOrder, limit, offset, search)Paginated list of systemd units.
setUnitStatus(name, action)Apply "start", "stop", or "restart".
logReplay(unit)Stream journal entries via SSE. Returns an AsyncGenerator.
logTail(unit, lines?, before?, after?, grep?, since?, until?, priority?)Page of journal entries with cursor-based pagination, grep, time range, and priority filtering.

Accounts

MethodDescription
authenticate(username, password)Returns session token and account.
createAccount(username, password, email, phone, realName, admin)Create a user. Password minimum 8 characters.
getAccount(username)Retrieve account by username.
updateAccount(username, fields)Modify account fields.
listAccounts(sortBy, sortOrder, limit, offset, search)Paginated list of accounts.
disableAccount(username)Prevent authentication.
enableAccount(username)Re-enable a disabled account.
listSessions(token)Active sessions for the token's user.
sessionUsername(token)Username for a session token.
revokeSession(sessionID)Invalidate a session.

Audit, Settings & Upgrades

MethodDescription
listAuditLog(opts)Paginated audit log with filters.
getSettings()All settings as key-value object.
getSetting(key)Single setting by key.
setSetting(key, value)Update a setting.
listUpgrades()Packages with newer versions available.
dismissUpgrades()Mark pending upgrades as dismissed.

Archives

MethodDescription
uploadArchive(subvolume, file, subpath?, stopService?)Upload and extract an archive via FormData. Returns {needs_restart, message}.
downloadArchive(subvolume, paths?, stopService?, format?)Download a subvolume archive. Returns raw Response for streaming.

Health

MethodDescription
ping()Service health and summary counts.

Development Reference

The Town OS backend runs on port 5309 with a Vite dev server on port 5173. Use make dev to start the full development environment.

Core Targets

TargetDescription
make devStart the full dev environment (backend + Vite dev server).
make dev-stopStop and remove the dev backend container.
make dev-logsTail journalctl inside the running dev container.
make dev-cleanStop the container and tear down the dev btrfs volume.

Testing Targets

TargetDescription
make testRun lint, Go unit tests, and JS unit tests.
make test-integrationRun Go integration tests in a privileged Podman container.
make test-ui-integrationRun Bun UI integration tests against a backend container.
make test-fullRun all test suites in sequence.
make auto-testWatch for file changes and re-run tests automatically.

Build Targets

TargetDescription
make production-imageBuild the production container image.
make test-imageBuild the test container image.
make pull-imagesPull base container images from Docker Hub.

Prerequisites

  • Go 1.25+
  • Bun — JavaScript runtime
  • Podman — rootful, with sudo
  • btrfs-progsmkfs.btrfs
  • golangci-lint

Create a .env file with repository credentials:

TOWN_OS_REPO_USERNAME=<username>
TOWN_OS_REPO_PASSWORD=<password>

After installing prerequisites, run make pull-images before any other targets.