Files
prompt/Server
TZGyn 1bd346ce20 Explain the SSL failure instead of just reporting it
A Postgres URL without an sslmode made lib/pq ask for TLS and die with
"SSL is not enabled on the server", which reads like a server fault when
it is really a connection-string default. The error now says what to
append, and the server retries briefly so a deploy is not marked
unhealthy just because Postgres was still starting.

The sync server URL also defaults to the hosted instance now, so a fresh
install needs no setup.
2026-10-08 10:38:54 +08:00
..

Prompt sync server

Small Go service that stores one JSON blob per sync group, keyed by an 8-digit code (1234-5678). There is no authentication: the code is the shared secret, so anyone who knows it can read and overwrite that group. It is meant to be self-hosted for personal use, not exposed as a public service.

Requirements

  • Postgres (any recent version; developed against 18)
  • A host both Macs can reach — a VPS, a home server, or a Tailscale node

Run it

createdb prompt_sync
cd Server
PROMPT_SYNC_DATABASE_URL="postgres://localhost:5432/prompt_sync?sslmode=disable" \
  go run .            # or: go build -o prompt-sync . && ./prompt-sync
Env var Default
PROMPT_SYNC_ADDR :8080
PROMPT_SYNC_DATABASE_URL postgres://localhost:5432/prompt_sync?sslmode=disable

The schema is created on startup. Every request is logged as METHOD PATH STATUS DURATION; SIGTERM/SIGINT shut down gracefully so an in-flight push is not cut off mid-write. If the database is not reachable yet it retries five times, 2s apart, before giving up.

sslmode matters. lib/pq defaults to sslmode=require, so a connection string copied from a host that runs Postgres without TLS (Coolify's internal Postgres, a plain docker run postgres, Homebrew) fails with pq: SSL is not enabled on the server. Append ?sslmode=disable in that case — the server prints exactly that hint when it hits this.

Deploying with Coolify

Use a Dockerfile application pointed at this repo with base directory Server/ (so Coolify builds Server/Dockerfile), expose port 8080, and set:

Variable Value
PROMPT_SYNC_DATABASE_URL your Postgres URL plus ?sslmode=disable
PROMPT_SYNC_ADDR :8080 (this is already the default)

Coolify detects the HEALTHCHECK in the Dockerfile and runs prompt-sync -healthcheck with its 5s start period, so the container only goes live once /healthz answers. It also logs a generic warning that a healthcheck needs curl or wget — that does not apply here, the binary does its own check, so there is no reason to add either to the image (it is distroless and has no shell).

Run with Docker

The image is a static binary on distroless (non-root, no shell), so its HEALTHCHECK calls prompt-sync -healthcheck, which pings the local /healthz — no curl needed in the image. That is what docker ps reports as the container health.

docker build -t prompt-sync Server/
docker run -d --name prompt-sync -p 8080:8080 \
  -e PROMPT_SYNC_DATABASE_URL="postgres://user:pass@host:5432/prompt_sync?sslmode=disable" \
  prompt-sync

docker logs -f prompt-sync         # request log

Postgres stays outside the container; the server only needs a URL. If Postgres runs on the Docker host itself, host is host.docker.internal on macOS and Windows, or the host's LAN address on Linux (--network host also works).

Deploy to a host

Build for the host's architecture and ship the image, or build it on the host itself (a git clone plus docker build is usually simplest):

# build on the Mac for a linux/arm64 host, then copy it over
docker build --platform linux/arm64 -t prompt-sync Server/
docker save prompt-sync | gzip > prompt-sync.tar.gz
scp prompt-sync.tar.gz <host>:
ssh <host> 'gunzip -c prompt-sync.tar.gz | docker load'

ssh <host> 'docker run -d --name prompt-sync --restart unless-stopped \
  -p 8080:8080 \
  -e PROMPT_SYNC_DATABASE_URL="postgres://user:pass@db-host:5432/prompt_sync?sslmode=disable" \
  prompt-sync'

Put it behind TLS if it is reachable from the internet — the code is the only secret, and it travels in the URL path. Both plain HTTP and HTTPS work from the app; an HTTP URL is often fine over a LAN or Tailscale.

Endpoints

Method Path Behaviour
POST /api/codes Generates a unique code, inserts an empty group
GET /api/sync/{code} {"version":N,"updatedAt":…,"data":{…}|null}, 404 if unknown
GET /api/sync/{code}/version {"version":N} — cheap poll used by the app
PUT /api/sync/{code} Upserts the payload, returns the incremented version
GET /healthz Liveness (200)

Payloads are opaque JSONB; the server never parses note contents. Bodies are capped at 50 MB, which is generous enough for base64 photos.

Backups

The whole state is one table, so pg_dump prompt_sync is a complete backup:

pg_dump prompt_sync | gzip > prompt_sync-$(date +%F).sql.gz