|
|
||
|---|---|---|
| cmd/server | ||
| internal/server | ||
| release-notes | ||
| scripts | ||
| .gitignore | ||
| AGENTS.md | ||
| LICENSE | ||
| README.md | ||
| README.ru.md | ||
| go.mod | ||
| go.sum | ||
| verstak-server.service | ||
README.md
Verstak Sync Server
Optional self-hosted synchronization relay for Verstak vaults.
English · Русский
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 persistentvault_id, then return a device tokenPOST /api/auth/test- Validate username/password from the desktop clientGET /api/client/me- Return current authenticated client/device detailsPOST /api/client/revoke-current- Revoke the current desktop device tokenPOST /api/client/revoke-device- Revoke another device owned by the same userPOST /api/v1/sync/push- Push local operations to the server operation logPOST /api/v1/sync/pull- Pull operations since a server sequence numberPOST /api/v1/blobs/- Store a multipartfileblob and return its SHA-256 hashGET /api/v1/blobs/{sha256}- Download a stored blob by SHA-256 hash
User API:
POST /api/v1/auth/register- Register a userGET /api/v1/auth/confirm?token=...- Display a confirmation form;POSTperforms confirmationPOST /api/v1/auth/login- User loginPOST /api/v1/auth/forgot- Request password resetPOST /api/v1/auth/reset- Reset passwordGET /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.