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.
117 lines
4.5 KiB
Markdown
117 lines
4.5 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
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):
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
pg_dump prompt_sync | gzip > prompt_sync-$(date +%F).sql.gz
|
|
```
|