Verstak Sync Server — HTTP API, auth/pairing, device registry, vault operation log, blob upload/download, conflict handling
 
 
 
 
Go to file
mirivlad 3bbeb42d37 Документация: обновлён AGENTS.md, release notes v0.1.0-alpha.4 2026-07-18 18:54:42 +08:00
cmd/server Harden sync server transport and credentials 2026-07-17 05:09:28 +08:00
internal/server fix(web): keep generated passwords out of page source 2026-07-17 07:04:46 +08:00
release-notes Документация: обновлён AGENTS.md, release notes v0.1.0-alpha.4 2026-07-18 18:54:42 +08:00
scripts fix(web): keep generated passwords out of page source 2026-07-17 07:04:46 +08:00
.gitignore Документация: обновлён AGENTS.md (API), удалены superpowers-планы, добавлен README.ru.md с шапкой 2026-07-18 18:06:49 +08:00
AGENTS.md Документация: обновлён AGENTS.md (API), удалены superpowers-планы, добавлен README.ru.md с шапкой 2026-07-18 18:06:49 +08:00
LICENSE docs: license sync server under AGPL 2026-07-12 23:06:33 +08:00
README.md Документация: обновлён AGENTS.md (API), удалены superpowers-планы, добавлен README.ru.md с шапкой 2026-07-18 18:06:49 +08:00
README.ru.md Документация: обновлён AGENTS.md (API), удалены superpowers-планы, добавлен README.ru.md с шапкой 2026-07-18 18:06:49 +08:00
go.mod feat: add server core (server, config, schema) 2026-06-20 02:01:23 +08:00
go.sum feat: add server core (server, config, schema) 2026-06-20 02:01:23 +08:00
verstak-server.service Harden sync server transport and credentials 2026-07-17 05:09:28 +08:00

README.md

Verstak Sync Server

Optional self-hosted synchronization relay for Verstak vaults.

English · Русский

Release Status License

Verstak Sync Server

Standalone sync server for Verstak2 platform.

Overview

This server provides synchronization between devices running Verstak2. It handles:

  • Device registration and authentication
  • Vault-scoped, ordered operation-log relay with server sequence numbers
  • Scoped content-addressed Blob transport for binary and large file content
  • User management with email confirmation

Quick Start

# Build (produces binary at build/bin/verstak-sync-server)
./scripts/build.sh

# Run
./build/bin/verstak-sync-server --data ./server-data

# First run with admin user
printf '%s\n' 'choose-a-long-password' > /tmp/verstak-admin-password
chmod 600 /tmp/verstak-admin-password
./build/bin/verstak-sync-server --admin-user admin --admin-pass-file /tmp/verstak-admin-password

Release packages

Build a Linux amd64 archive locally:

./scripts/release.sh v0.1.0-alpha.1

It runs the server build and tests, then writes release/verstak-sync-server-linux-amd64-<version>.tar.gz and SHA256SUMS. The archive contains the server binary, systemd service file and install script.

Publish those same assets to GitHub Releases:

./scripts/publish-github-release.sh v0.1.0-alpha.1

The publisher requires an authenticated gh CLI and a clean local main equal to origin/main. It creates and pushes an annotated tag when necessary, then creates or updates the GitHub Release.

Configuration

Flag Default Description
--listen 127.0.0.1:47732 HTTP address; an administrator must explicitly expose another interface
--port Deprecated compatibility shortcut; always binds loopback
--data ./server-data Data directory
--admin-user Create admin user (first run)
--admin-pass-file Read the initial admin password from a protected file
--admin-pass-stdin Read the initial admin password from stdin

The server has explicit header/read/write/idle timeouts and a 16 KiB header limit. It handles SIGINT/SIGTERM with a 20-second graceful shutdown and closes SQLite afterwards. Release builds publish version and build_commit through the health response; neither logs nor health contain credentials.

config.yml can set listen, public_url, trusted_proxies, and limits:

listen: 127.0.0.1:47732
public_url: https://sync.example.test
trusted_proxies: [127.0.0.1, ::1]
limits:
  max_json_body: 2097152
  max_push_operations: 100
  max_payload_json: 262144
  max_pull_page: 100
  max_blob_bytes: 268435456
  max_vault_blob_bytes: 4294967296
  max_user_blob_bytes: 8589934592
retention:
  idempotency_hours: 24
  audit_days: 90
  temp_upload_hours: 24
web:
  # Server default; visitors may choose System, Русский, or English in a cookie.
  default_locale: en
  # Set false for invite/admin-only installations.
  allow_registration: true
  # Product name rendered in the embedded web console.
  server_name: Verstak Sync Server

Production installs use:

  • binary: /opt/verstak-sync-server/verstak-sync-server;
  • data directory: /var/lib/verstak-sync-server;
  • listen-address environment file: /etc/verstak-server/env;
  • service: verstak-server.

Install from a built binary:

./scripts/build.sh
sudo ./scripts/install.sh \
  --bin ./build/bin/verstak-sync-server \
  --listen 127.0.0.1:47732 \
  --admin-user admin \
  --admin-pass-file /root/verstak-admin-password

The install script creates a locked-down system user, initializes the data directory, writes /etc/verstak-server/env, installs the systemd unit, and starts the service.

Deployment

Run the service behind HTTPS in production. The sync server itself listens on plain HTTP; terminate TLS in a reverse proxy such as nginx, Caddy, or a platform load balancer, then forward to 127.0.0.1:47732.

Basic service operations:

sudo systemctl status verstak-server
sudo journalctl -u verstak-server -f
curl http://127.0.0.1:47732/api/v1/health

Change the listen port:

echo 'VERSTAK_LISTEN=127.0.0.1:47733' | sudo tee /etc/verstak-server/env
sudo systemctl restart verstak-server

Upgrade the binary:

./scripts/build.sh
sudo systemctl stop verstak-server
sudo install -m 755 ./build/bin/verstak-sync-server /opt/verstak-sync-server/verstak-sync-server
sudo systemctl start verstak-server

Keep --data stable across upgrades. The data directory is the durable relay log for connected devices; each Desktop vault remains the source of truth for its local-first files and workspace state.

Backup And Restore

Back up the full data directory while the service is stopped. It contains:

  • server.db - SQLite database with users, devices, operations, SMTP settings, and blob metadata;
  • config.yml - admin user configuration;
  • blobs/ - content-addressed blob files.

Create a backup:

sudo systemctl stop verstak-server
sudo tar --xattrs --acls -czf verstak-sync-backup-$(date +%Y%m%d-%H%M%S).tar.gz \
  -C /var/lib verstak-sync-server
sudo systemctl start verstak-server

Restore onto a fresh host or after data loss:

sudo systemctl stop verstak-server
sudo mv /var/lib/verstak-sync-server /var/lib/verstak-sync-server.broken.$(date +%Y%m%d-%H%M%S) 2>/dev/null || true
sudo tar --xattrs --acls -xzf verstak-sync-backup-YYYYMMDD-HHMMSS.tar.gz -C /var/lib
sudo chown -R verstak:verstak /var/lib/verstak-sync-server
sudo chmod 750 /var/lib/verstak-sync-server
sudo systemctl start verstak-server
curl http://127.0.0.1:47732/api/v1/health

After restore, connected desktop clients keep their existing device tokens. If a backup is older than some client changes, those clients may need to run sync again so unpushed local operations are re-sent.

Architecture

cmd/server/          - Entry point
internal/server/     - Server implementation
  - server.go        - Core server logic
  - routes.go        - HTTP routing
  - handlers_api.go  - Sync, client, health, and blob handlers
  - handlers_auth.go - User auth API handlers
  - handlers_admin.go - Admin web/API handlers
  - schema.go        - Database schema

API Endpoints

Desktop sync client:

  • POST /api/client/pair - Pair a desktop client with username/password and its persistent vault_id, then return a device token
  • POST /api/auth/test - Validate username/password from the desktop client
  • GET /api/client/me - Return current authenticated client/device details
  • POST /api/client/revoke-current - Revoke the current desktop device token
  • POST /api/client/revoke-device - Revoke another device owned by the same user
  • POST /api/v1/sync/push - Push local operations to the server operation log
  • POST /api/v1/sync/pull - Pull operations since a server sequence number
  • POST /api/v1/blobs/ - Store a multipart file blob and return its SHA-256 hash
  • GET /api/v1/blobs/{sha256} - Download a stored blob by SHA-256 hash

User API:

  • POST /api/v1/auth/register - Register a user
  • GET /api/v1/auth/confirm?token=... - Display a confirmation form; POST performs confirmation
  • POST /api/v1/auth/login - User login
  • POST /api/v1/auth/forgot - Request password reset
  • POST /api/v1/auth/reset - Reset password
  • GET /api/v1/user/devices - List devices for the current user session

Operational endpoints:

  • GET /api/v1/health - Server health and basic storage status
  • /admin/... - Admin web UI and admin JSON endpoints
  • /register, /login, /dashboard, /forgot, /reset, /logout - User web UI

Embedded web console

The server embeds its public, account, and administrator interface in the Go binary. It has no CDN, npm build, external font, or remote analytics dependency. / is a localized public page; /login, /register, /forgot, and /reset use post/redirect/get flows. /dashboard lets a signed-in user review and search only their own devices, see confirmation state and connection times, and revoke one only after entering their password. The reusable layout, templates, CSS, JavaScript, and local SVG live below internal/server/web/ and are embedded with go:embed; there is no separate frontend build.

/admin/login opens the administrator console. Its sidebar provides overview, users, devices, vaults, storage, audit, SMTP settings, and diagnostics. Lists use bounded server-side search, filters, whitelisted sort order, and pagination. Administrators can create, edit, confirm, block, reset, and delete users; revoke devices; and permanently remove only a previously revoked device. A browser password reset generates a random password and exposes it once through a CSRF-protected no-store POST after the result page loads; it is never placed in the URL, initial HTML source, audit log, cookies, or database plaintext. Destructive browser actions use the shared local confirmation dialog. Blocking, credential changes, device actions, cleanup, and SMTP changes require the current administrator password again. Admin HTML is deliberately a normal server rendered control plane; the existing /admin/api/... endpoints remain for automation.

The locale resolver uses the verstak_locale HttpOnly/Lax cookie first. Its values are ru, en, or system; system uses Accept-Language, then web.default_locale, then English. The choice survives login and logout. A separate short-lived HttpOnly form token protects the language chooser and all anonymous browser POST forms; it is distinct from the server-side session CSRF token. Registration is controlled by web.allow_registration; when disabled the public registration page does not expose account creation.

The overview reports operational counts and readiness warnings. Vault details show only metadata (devices, sequence, operation count, activity, and blob usage), never file contents or operation payload_json. Diagnostics download is sanitized: it excludes paths, tokens, passwords, hashes, and payloads. General web settings are stored in the existing config.yml; public URL and registration policy can be changed there through the console, while transport limits remain read-only. SMTP passwords are never returned to a browser form.

All browser mutations use POST and validate a server-side session plus CSRF token; anonymous forms use the separate public-form token described above. The server returns security headers including a restrictive CSP, frame-ancestors 'none', nosniff, and a same-origin referrer policy. The console must still be deployed behind the HTTPS reverse proxy described below: secure cookies are enabled when HTTPS is detected through a trusted proxy.

Run the local interactive browser smoke (Chromium plus Node's built-in undici; no npm install) with:

./scripts/smoke-web.sh

It exercises language switching, admin login/navigation, temporary-user creation, block/unblock confirmation, device pairing/revocation, filtering, logout, and desktop/mobile screenshots. It starts an isolated temporary server and removes its data and screenshots on exit; it is not a reverse-proxy or production-deployment test.

Sync operations are generic records with entity_type, entity_id, op_type, payload_json, device_id, and sequencing metadata. A pairing token is bound to one user and vault. The server derives the stored device ID and operation scope from that token, ignores a caller-supplied device_id for authorization, and returns only operations and cursors from the authenticated user/vault. Operations are returned in increasing server_sequence; clients must stop at the first operation they cannot apply and retry that sequence later. The server does not merge files, resolve conflicts, or create replacement names.

The desktop pairing payload may supply an existing vault_id to add a new empty local vault to that remote scope. The server treats that value only as a scope selector: reconciliation, conflict detection, snapshots, and durable workspace identity remain Desktop-core responsibilities. Small text can remain inline; binary and large files are uploaded first and their operations carry a blob {sha256,size} reference. Blob bytes are physically deduplicated but a user_id/vault_id reference is mandatory. Knowing another scope's SHA-256 never grants download access. Revoked devices and blocked users lose sync and blob access immediately.

POST /api/v1/sync/pull accepts since_sequence and optional page_limit. The response has ordered ops, page_last_sequence, server_sequence, and has_more. Clients must persist a cursor only after applying each operation. Push is capped by the configured JSON/body/field limits and returns stable JSON {error,code} errors (including request_too_large, rate_limited, and quota_exceeded); desktop and UI localize codes rather than server text.

New device tokens, sessions, and email/reset tokens are stored only as SHA-256 hashes. The plaintext device token is returned once by pairing. Older plaintext API keys are marked legacy_api_key=1 during migration and are never created again; rotate/re-pair them during normal deployment. Admin key endpoints show only a prefix/suffix hint. Sessions are database-backed, expire after 24 hours, rotate on login, and use HttpOnly/Lax cookies plus a SameSite/CSRF companion cookie. Mutating browser endpoints require a matching CSRF token and do not use GET for destructive work.

Reverse proxy and TLS

TLS terminates at nginx or Caddy; the server has no built-in TLS. It ignores Forwarded, X-Forwarded-For, and X-Forwarded-Proto unless the TCP peer is listed in trusted_proxies. For a local nginx/Caddy proxy use trusted_proxies: [127.0.0.1, ::1] and set public_url to the HTTPS URL.

location / {
  proxy_pass http://127.0.0.1:47732;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
}

Never bind 0.0.0.0 merely to make a proxy work. If an external listener is intentional, restrict it with a firewall and configure the real proxy CIDR.

Operations, retention, and privacy

GET /api/v1/health, /livez, and /readyz report status, version, build, uptime, database reachability, blob writability, schema version, and server time without paths or secrets. The internal stats service exposes user/device, vault, operation, database/blob size, and last-sync counters for a future admin panel.

Retention cleans expired sessions/email tokens, bounded idempotency records, old audit entries, stale upload temp files, and in-memory rate buckets. It does not delete sync operations or referenced blobs: without a materialized checkpoint and verified recovery protocol, pruning would prevent a new device from restoring a vault. That checkpoint/operation-retention design is a future milestone.

The server is optional: Desktop remains local-first. The relay can see metadata and file bytes needed to serve operations/blobs; files are not end-to-end encrypted in this milestone. Secrets, plugin settings, Todo, Journal, Activity, and Browser Inbox are not synchronized here.

New device enrollment requires a non-empty vault_id. The legacy: prefix is reserved for server-side migration of older records and cannot be selected by new clients.

Development

# Run tests
go test ./...

# Run real headless Chromium smoke screenshots in a temporary directory
./scripts/smoke-web.sh

# Build for production
CGO_ENABLED=1 go build -o verstak-sync-server ./cmd/server

License

Copyright © 2026 Verstak contributors. Licensed under GNU AGPLv3 or later.