API Documentation
Complete reference for the Town OS systemcontroller API — 51 endpoints across accounts, storage, repositories, packages, systemd, settings, audit, and status.
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:
| Level | Description |
|---|---|
| Public | No token required. |
| Authenticated | Any valid session token. |
| Admin | Session token belonging to an admin account. |
Pagination
All list endpoints accept the following query parameters and return a common envelope:
| Parameter | Type | Description |
|---|---|---|
sort_by | string | Field name to sort on. |
sort_order | string | asc or desc. |
limit | int | Page size (default 20). |
offset | int | Pagination offset. |
search | string | Case-insensitive substring match across all string fields. |
Response Envelope
{
"entries": [...],
"has_more": true,
"total_pages": 5,
"total_count": 97
} Status
/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
/account/authenticate Public Authenticate with username and password. Returns a session token and the account object.
| Field | Type | Description |
|---|---|---|
username | string | Required. Account username. |
password | string | Required. Account password. |
/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.
| Field | Type | Description |
|---|---|---|
username | string | Required. |
password | string | Required. Minimum 8 characters. |
email | string | Required. |
phone | string | Required. |
real_name | string | Required. |
admin | boolean | Whether the account has admin privileges. |
/account Authenticated
Get a single account by username. Request body: {"username": "alice"}.
/account Authenticated List all accounts. Supports pagination parameters.
/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.
/account/me Authenticated Returns the username associated with the token in the Authorization header.
/account/sessions Authenticated List all active sessions for the authenticated user. Each session includes its ID, username, creation time, and last-used time.
/account/session/revoke Authenticated
Revoke a session by ID. Request body: {"session_id": "..."}.
/account/disable Admin
Disable an account. Request body: {"username": "bob"}.
/account/enable Admin
Re-enable a disabled account. Request body: {"username": "bob"}.
Storage
/storage Authenticated
List filesystems. Accepts pagination parameters plus optional name
(prefix filter) and state (user, installed,
or uninstalled) in the request body.
/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.
/storage/modify Authenticated
Modify an existing filesystem. Send name to identify it and a
filesystem object with the updated name and/or quota.
/storage/remove Authenticated
Remove a filesystem. Request body: {"name": "mydata"}.
/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.
| Setting | Default | Description |
|---|---|---|
max_archive_size | 20 MB | Maximum upload size. |
archive_unpack_timeout | 120 seconds | Maximum time for unpacking. |
/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
/repository Authenticated List all configured package repositories with name, URL, and any error status. Supports pagination parameters.
/repository/add Authenticated Add a new package repository. Triggers an immediate refresh.
| Field | Type | Description |
|---|---|---|
name | string | Required. Display name for the repository. |
url | string | Required. Git URL of the repository. |
username | string | Optional. Auth username for private repos. |
password | string | Optional. Auth password for private repos. |
/repository/remove Authenticated
Remove a repository by name. Triggers an immediate refresh.
Request body: {"name": "my-repo"}.
/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}.
/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
/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.
/packages/by-repo Authenticated
List packages grouped by repository. Accepts an optional search query
parameter. Returns an array of {"repo": "...", "packages": [...]} groups.
/packages/timezones Authenticated List all available IANA timezone names for use in package configuration.
/packages/installed Authenticated List installed package identifiers. Supports pagination parameters.
/packages/installed/info Authenticated
Get detailed info for an installed package. Send repo, name,
and version. Returns questions, user responses, notes, and note types.
/packages/responses Authenticated
Get the saved question responses for an installed package. Send repo,
name, and version. Returns a key-value map of responses.
/packages/versions Authenticated
List available versions for a package. Request body: {"name": "nginx"}.
Returns a string array of version identifiers.
/packages/children Authenticated
List child packages. Send repo and name. Returns a string array.
/packages/questions Admin
Get the installation questions for a package. Request body: {"name": "nginx"}.
Returns a map of question key to {"query": "...", "type": "..."}.
/packages/questions/identity Admin
Get questions for a specific package version. Send repo, name,
and version.
/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.
/packages/install Admin Install a package.
| Field | Type | Description |
|---|---|---|
repo | string | Required. Repository name. |
name | string | Required. Package name. |
version | string | Required. Version to install. |
responses | object | Required. Key-value answers to installation questions. |
reuse_volumes | boolean | Reuse existing data volumes from a previous installation. |
import_from_version | string | Version to import volumes from during upgrade. |
/packages/uninstall Admin
Uninstall a package. Send repo, name, version,
and optional purge_volumes (boolean) to delete associated data.
/packages/disable Admin
Disable an installed package (stop its service). Send repo and name.
/packages/enable Admin
Re-enable a disabled package (start its service). Send repo and name.
/packages/purge-volumes Admin
Delete all data volumes for an installed package. Send repo and name.
/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.
/packages/purge-uninstalled-volumes Admin
Delete leftover volumes from previously uninstalled versions. Send repo
and name.
/packages/upgrades Authenticated
List available upgrades for installed packages. Each entry includes
installed_version, latest_version, and whether the package
definition has changed.
/packages/upgrades/dismiss Admin Dismiss the current upgrade notifications. Send an empty JSON object.
Systemd
/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.
/systemd/status Admin
Control a systemd unit. Send name (unit name) and action
(start, stop, or restart).
/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.
/systemd/logs/tail Authenticated Fetch a page of journal entries with cursor-based pagination and filtering.
| Parameter | Type | Description |
|---|---|---|
unit | string | Required. Systemd unit name. |
lines | int | Required. Number of entries to return. |
before | string | Cursor — return entries before this position. |
after | string | Cursor — return entries after this position. |
grep | string | Case-insensitive substring filter on message text. |
since | int | Unix timestamp — return entries from this time forward. |
until | int | Unix timestamp — stop collecting at this time. |
Returns entries, cursor (first entry), and
end_cursor (last entry) for subsequent pagination.
Settings
/settings Admin Get all settings as a key-value object.
/settings/get Admin
Get a single setting. Request body: {"key": "default_quota"}.
Returns key and value.
/settings/set Admin
Set a setting value. Request body: {"key": "default_quota", "value": "107374182400"}.
Default Settings
| Key | Default | Description |
|---|---|---|
default_quota | 53687091200 (50 GB) | Default quota for new filesystems. |
max_archive_size | 20971520 (20 MB) | Maximum archive upload size. |
archive_unpack_timeout | 120 (seconds) | Maximum time for archive unpacking. |
Audit Log
/audit/log Admin List audit log entries. All fields in the request body are optional.
| Field | Type | Description |
|---|---|---|
before_id | int | Keyset pagination — return entries with ID less than this. |
account | string | Filter by account username. |
sort_by | string | Field to sort on. |
sort_order | string | asc or desc. |
limit | int | Page size. |
offset | int | Pagination offset. |
search | string | Search 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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Target | Description |
|---|---|
make dev | Start the full dev environment (backend + Vite dev server). |
make dev-stop | Stop and remove the dev backend container. |
make dev-logs | Tail journalctl inside the running dev container. |
make dev-clean | Stop the container and tear down the dev btrfs volume. |
Testing Targets
| Target | Description |
|---|---|
make test | Run lint, Go unit tests, and JS unit tests. |
make test-integration | Run Go integration tests in a privileged Podman container. |
make test-ui-integration | Run Bun UI integration tests against a backend container. |
make test-full | Run all test suites in sequence. |
make auto-test | Watch for file changes and re-run tests automatically. |
Build Targets
| Target | Description |
|---|---|
make production-image | Build the production container image. |
make test-image | Build the test container image. |
make pull-images | Pull base container images from Docker Hub. |
Prerequisites
- Go 1.25+
- Bun — JavaScript runtime
- Podman — rootful, with
sudo - btrfs-progs —
mkfs.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.