Self-hosting meant building the binary by hand and wiring Postgres up
yourself. The image is a static binary on distroless with a built-in
healthcheck flag, and docker-compose brings up Postgres alongside it.
The image itself is unbuilt here: no container runtime is available in
this environment, so only the static Linux build and the healthcheck
flag have been exercised.
💘 Generated with Crush
Assisted-by: Crush:kimi-k3
94 lines
3.2 KiB
Markdown
94 lines
3.2 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.
|
|
|
|
## Run with Docker
|
|
|
|
`docker-compose.yml` starts Postgres and the server together:
|
|
|
|
```bash
|
|
cd Server
|
|
POSTGRES_PASSWORD=$(openssl rand -hex 16) docker compose up -d
|
|
docker compose logs -f server # request log
|
|
```
|
|
|
|
Or build the image on its own and point it at an existing Postgres:
|
|
|
|
```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
|
|
```
|
|
|
|
The image is a static binary on `distroless` (non-root, no shell, ~6 MB binary),
|
|
so its `HEALTHCHECK` calls `prompt-sync -healthcheck`, which pings the local
|
|
`/healthz` and exits non-zero on failure — no curl needed in the image. That is
|
|
what `docker ps` reports as the container health.
|
|
|
|
## Deploy to a Linux host
|
|
|
|
```bash
|
|
# from the repo root, on the Mac
|
|
cd Server
|
|
GOOS=linux GOARCH=arm64 go build -o prompt-sync . # amd64 for x86 hosts
|
|
scp prompt-sync <host>:/tmp/ && ssh <host> 'sudo mv /tmp/prompt-sync /usr/local/bin/'
|
|
|
|
# on the host: create the database and role, then
|
|
sudo cp Server/prompt-sync.service /etc/systemd/system/
|
|
sudo systemctl edit --full prompt-sync # set PROMPT_SYNC_DATABASE_URL
|
|
sudo systemctl enable --now prompt-sync
|
|
journalctl -u prompt-sync -f # verify it is listening
|
|
```
|
|
|
|
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
|
|
```
|