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.
4.5 KiB
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.
sslmodematters. lib/pq defaults tosslmode=require, so a connection string copied from a host that runs Postgres without TLS (Coolify's internal Postgres, a plaindocker run postgres, Homebrew) fails withpq: SSL is not enabled on the server. Append?sslmode=disablein 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