Skip to content

Server setup

This procedure creates an empty disposable database named synchro_quickstart. It creates one public.notes relation for the canonical authenticated user alice.

Use a fresh disposable Ubuntu 24.04 x64 VM and a fresh PostgreSQL 18 cluster. Use the matching PostgreSQL 18 extension artifact. This is the packaged server support environment. Do not apply this procedure to an existing PostgreSQL cluster. Do not use it with production data.

You need:

  • a PostgreSQL 18 package installation;
  • a matching synchro_pg PostgreSQL 18 Ubuntu 24.04 x64 artifact;
  • the matching synchrod-pg binary;
  • Git, GitHub CLI (gh), curl, openssl, Python 3, and psql;
  • a PostgreSQL superuser for installation and cluster configuration.

Run the shell examples in Bash from a checkout of the selected Synchro version. Keep that checkout unchanged while you install its matching artifacts.

The Synchro 0.4.0-rc.4 server artifact uses PostgreSQL 18.3. Install that version in the fresh VM from the PostgreSQL APT archive:

Terminal window
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl --fail --silent --show-error \
--output /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
https://www.postgresql.org/media/keys/ACCC4CF8.asc
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt-archive.postgresql.org/pub/repos/apt noble-pgdg-archive main" |
sudo tee /etc/apt/sources.list.d/pgdg-archive.list >/dev/null
sudo apt-get update
sudo apt-get install -y \
postgresql-18=18.3-1.pgdg24.04+1 \
postgresql-client-18=18.3-1.pgdg24.04+1 \
postgresql-server-dev-18=18.3-1.pgdg24.04+1
sudo pg_createcluster 18 synchro_tutorial --port 55432 --start

The published artifact name is synchro-pg-pg18-ubuntu24.04-linux-x64-<version>.tar.gz. Download the artifact that matches VERSION from the matching release and verify its published checksum before extraction:

Terminal window
export SYNCHRO_VERSION="$(tr -d '\n' < VERSION)"
export SYNCHRO_ARTIFACT_NAME="synchro-pg-pg18-ubuntu24.04-linux-x64-${SYNCHRO_VERSION}.tar.gz"
export SYNCHRO_DOWNLOAD_DIR="$(mktemp -d)"
mkdir "$SYNCHRO_DOWNLOAD_DIR/artifacts"
gh release download "v${SYNCHRO_VERSION}" \
--repo trainstar/synchro \
--pattern "$SYNCHRO_ARTIFACT_NAME" \
--pattern "synchrod-pg-linux-x64-${SYNCHRO_VERSION}" \
--dir "$SYNCHRO_DOWNLOAD_DIR/artifacts"
gh release download "v${SYNCHRO_VERSION}" \
--repo trainstar/synchro --pattern SHA256SUMS --dir "$SYNCHRO_DOWNLOAD_DIR"
test -f "$SYNCHRO_DOWNLOAD_DIR/artifacts/$SYNCHRO_ARTIFACT_NAME"
test -f "$SYNCHRO_DOWNLOAD_DIR/artifacts/synchrod-pg-linux-x64-${SYNCHRO_VERSION}"
(cd "$SYNCHRO_DOWNLOAD_DIR" && sha256sum --check --ignore-missing SHA256SUMS)
export SYNCHRO_EXTENSION_ARCHIVE="$SYNCHRO_DOWNLOAD_DIR/artifacts/$SYNCHRO_ARTIFACT_NAME"
export SYNCHRO_ADAPTER="$SYNCHRO_DOWNLOAD_DIR/artifacts/synchrod-pg-linux-x64-${SYNCHRO_VERSION}"
chmod 0755 "$SYNCHRO_ADAPTER"

Alternatively, before a release exists, build from the clean Linux x64 checkout. This path requires Go 1.25, Rust 1.92, and cargo-pgrx 0.17. Configure pgrx for the installed PostgreSQL version:

Terminal window
cargo pgrx init --pg18 /usr/lib/postgresql/18/bin/pg_config
make release-stage-server VERSION="$(tr -d '\n' < VERSION)" \
PGRX_PG_CONFIG=/usr/lib/postgresql/18/bin/pg_config
export SYNCHRO_VERSION="$(tr -d '\n' < VERSION)"
export SYNCHRO_EXTENSION_ARCHIVE="$PWD/dist/release-components/server/extension/synchro-pg-pg18-ubuntu24.04-linux-x64-${SYNCHRO_VERSION}.tar.gz"
export SYNCHRO_ADAPTER="$PWD/dist/release-components/server/adapter/synchrod-pg-linux-x64-${SYNCHRO_VERSION}"

The local archive is dist/release-components/server/extension/synchro-pg-pg18-ubuntu24.04-linux-x64-$(tr -d '\n' < VERSION).tar.gz. The archive contains one synchro_pg.control, one synchro_pg.so, one base script synchro_pg--<version>.sql, and zero or more update scripts synchro_pg--<from>--<to>.sql. The procedure requires exactly one control file, exactly one library, and exactly one base script synchro_pg--${SYNCHRO_VERSION}.sql. It installs the control file, the library, and every synchro_pg--*.sql file in the archive.

Terminal window
: "${SYNCHRO_EXTENSION_ARCHIVE:?Select published or locally built artifacts first}"
: "${SYNCHRO_ADAPTER:?Select the matching adapter binary first}"
: "${SYNCHRO_VERSION:?Select the Synchro version first}"
export SYNCHRO_UNPACK_DIR="$(mktemp -d)"
tar -xzf "$SYNCHRO_EXTENSION_ARCHIVE" -C "$SYNCHRO_UNPACK_DIR"
test "$(find "$SYNCHRO_UNPACK_DIR" -type f -name synchro_pg.control | wc -l)" -eq 1
test "$(find "$SYNCHRO_UNPACK_DIR" -type f -name "synchro_pg--${SYNCHRO_VERSION}.sql" | wc -l)" -eq 1
test "$(find "$SYNCHRO_UNPACK_DIR" -type f -name synchro_pg.so | wc -l)" -eq 1
control="$(find "$SYNCHRO_UNPACK_DIR" -type f -name synchro_pg.control -print -quit)"
library="$(find "$SYNCHRO_UNPACK_DIR" -type f -name synchro_pg.so -print -quit)"
test -n "$control" && test -n "$library"
pg_config=/usr/lib/postgresql/18/bin/pg_config
sudo install -m 0644 "$control" "$("$pg_config" --sharedir)/extension/"
find "$SYNCHRO_UNPACK_DIR" -type f -name 'synchro_pg--*.sql' \
-exec sudo install -m 0644 {} "$("$pg_config" --sharedir)/extension/" \;
sudo install -m 0755 "$library" "$("$pg_config" --pkglibdir)/"

The control, SQL, and library files must come from one release artifact. Do not mix artifact versions.

2. Configure PostgreSQL before extension creation

Section titled “2. Configure PostgreSQL before extension creation”

Create /etc/postgresql/18/synchro_tutorial/conf.d/synchro.conf. Preserve other shared_preload_libraries values when you edit the setting.

Terminal window
sudo install -d /etc/postgresql/18/synchro_tutorial/conf.d
sudo tee /etc/postgresql/18/synchro_tutorial/conf.d/synchro.conf >/dev/null <<'CONF'
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
shared_preload_libraries = 'synchro_pg'
synchro.auto_start = off
synchro.database = 'synchro_quickstart'
synchro.worker_login = 'synchro_quickstart_worker'
synchro.replication_slot = 'synchro_quickstart_slot'
synchro.publication_name = 'synchro_quickstart_pub'
CONF

Restart PostgreSQL. wal_level, shared_preload_libraries, and the worker settings require a restart.

Terminal window
sudo pg_ctlcluster 18 synchro_tutorial restart

3. Create the database and separated roles

Section titled “3. Create the database and separated roles”

Create passwords with the prompts. Do not put passwords in shell history, SQL files, or source control.

Terminal window
sudo -u postgres createdb --host /var/run/postgresql --port 55432 synchro_quickstart
sudo -u postgres createuser --host /var/run/postgresql --port 55432 \
--login --inherit --pwprompt \
--no-superuser --no-createdb --no-createrole --no-replication \
synchro_quickstart_owner
sudo -u postgres createuser --host /var/run/postgresql --port 55432 \
--login --inherit --pwprompt \
--no-superuser --no-createdb --no-createrole --no-replication \
synchro_quickstart_adapter
sudo -u postgres createuser --host /var/run/postgresql --port 55432 \
--login --no-inherit --no-password \
--no-superuser --no-createdb --no-createrole --replication \
synchro_quickstart_worker
sudo -u postgres psql -X -v ON_ERROR_STOP=1 \
--host /var/run/postgresql --port 55432 --dbname synchro_quickstart <<'SQL'
CREATE EXTENSION synchro_pg;
GRANT synchro_operator TO synchro_quickstart_owner;
GRANT synchro_adapter TO synchro_quickstart_adapter;
GRANT synchro_worker TO synchro_quickstart_worker;
GRANT CREATE ON SCHEMA public TO synchro_quickstart_owner;
GRANT USAGE ON SCHEMA public TO synchro_owner, synchro_worker;
GRANT CREATE ON DATABASE synchro_quickstart TO synchro_quickstart_owner;
GRANT CONNECT ON DATABASE synchro_quickstart
TO synchro_quickstart_owner, synchro_quickstart_adapter,
synchro_quickstart_worker;
SET ROLE synchro_quickstart_owner;
CREATE PUBLICATION synchro_quickstart_pub;
RESET ROLE;
SQL

The worker login must have REPLICATION, NOINHERIT, and only the synchro_worker membership. The owner and adapter are separate login roles. The extension creates the synchro_* group roles. Do not grant application roles direct access to Synchro internal tables. The background worker opens an internal PostgreSQL connection with its configured worker login. It does not use a password or a pg_hba.conf rule.

The application owner must own both the registered relations and the configured publication. The empty publication above establishes that ownership before registration. It does not need membership in the internal synchro_owner role.

Enable the worker after the database and worker login exist:

Terminal window
sudo sed -i "s/^synchro\.auto_start = off$/synchro.auto_start = on/" \
/etc/postgresql/18/synchro_tutorial/conf.d/synchro.conf
sudo pg_ctlcluster 18 synchro_tutorial restart

Run the checked-in setup file from the repository root as the owner role. Supply its password at the prompt. The file creates public.notes, enables RLS, grants only the required owner and worker permissions, creates a projection-only membership function, and registers the relation. It does not create or connect to a database. It applies to the database named by the psql command.

Terminal window
psql --host 127.0.0.1 --username synchro_quickstart_owner \
--port 55432 --dbname synchro_quickstart -X -v ON_ERROR_STOP=1 \
--file docs/examples/quickstart.sql

The membership function reads only synchro_projection.notes. It returns user:{owner_id}. The worker evaluates the function as its owner, synchro_quickstart_owner, in a security-restricted operation. synchro_prepare_projection_view grants that role SELECT on the view. The RLS policy compares owner_id with the authenticated push identity in synchro.user_id when that context exists. It prevents a user from writing another user’s row or changing owner_id. A missing context permits trusted database owner and operator maintenance. The adapter login cannot issue arbitrary application SQL. The first row is not inserted yet.

Confirm that the owner role created and registered the relation:

Terminal window
psql --host 127.0.0.1 --username synchro_quickstart_owner \
--port 55432 --dbname synchro_quickstart -X -v ON_ERROR_STOP=1 \
--command "SELECT to_regclass('public.notes') IS NOT NULL AS notes_created"

Run this block in Bash. It generates a temporary local HS256 secret without printing it:

Terminal window
export JWT_SECRET="$(openssl rand -hex 32)"
export DATABASE_URL='postgres://synchro_quickstart_adapter@127.0.0.1:55432/synchro_quickstart'
export LISTEN_ADDR=127.0.0.1:8091
export MIN_CLIENT_VERSION=0.3.0
read -rsp 'Adapter database password: ' PGPASSWORD
echo
export PGPASSWORD
"$SYNCHRO_ADAPTER" &
adapter_pid=$!
unset PGPASSWORD

Do not add the adapter password to DATABASE_URL or shell history. The background process keeps its inherited password only for its own connection.

Wait for capture readiness. Registration must finish before the first source write.

Terminal window
for attempt in $(seq 1 90); do
if test "$(curl --fail --silent http://127.0.0.1:8091/ready)" = '{"ready":true}'; then
break
fi
sleep 1
done
test "$(curl --fail --silent http://127.0.0.1:8091/ready)" = '{"ready":true}'

http://127.0.0.1:8091 is only for this local tutorial. Production adapters must bind privately and run behind a reverse proxy that terminates HTTPS.

When the Ubuntu tutorial VM is remote, run this command in a separate terminal on the Swift or React Native development Mac. Set TUTORIAL_SSH_HOST to the private SSH host name or address. The command keeps the tutorial HTTP listener private and forwards it to the development host loopback address.

Terminal window
printf 'Tutorial SSH host (user@host): '
read -r TUTORIAL_SSH_HOST
ssh -N -L 127.0.0.1:8091:127.0.0.1:8091 "$TUTORIAL_SSH_HOST"

Swift and React Native development on that Mac use http://127.0.0.1:8091. The Android emulator uses http://10.0.2.2:8091 to reach the Mac loopback forward. Do not use either address for a physical device. A physical device has its own localhost and needs a private reachable endpoint with HTTPS.

6. Insert and observe the first server row

Section titled “6. Insert and observe the first server row”

Insert only after readiness returns true:

Terminal window
psql --host 127.0.0.1 --username synchro_quickstart_owner \
--port 55432 --dbname synchro_quickstart -X -v ON_ERROR_STOP=1 <<'SQL'
INSERT INTO public.notes (id, owner_id, body)
VALUES (
'00000000-0000-4000-8000-000000000001',
'alice',
'Initial server note'
);
SELECT id, owner_id, body
FROM public.notes
WHERE id = '00000000-0000-4000-8000-000000000001';
SQL

The query must return the UUID, alice, and Initial server note. The row belongs to user:alice.

The tutorial uses the default user:alice assignment. The server also supports fixed shared scopes, explicit grants, and one assignment function. Run these statements as an operator:

SELECT synchro.synchro_register_shared_scope('notes:announcements', false);
SELECT synchro.synchro_grant_user_scope('alice', 'notes:review');
SELECT synchro.synchro_revoke_user_scope('alice', 'notes:review');

An assignment function derives scopes from application data. This example assigns review scopes from an application table. Run it as synchro_quickstart_owner, because that role has synchro_operator membership and must own the function:

CREATE TABLE public.note_reviewers (
user_id text NOT NULL,
scope_id text NOT NULL,
PRIMARY KEY (user_id, scope_id)
);
CREATE FUNCTION public.note_review_scopes(p_user_id text)
RETURNS SETOF text
LANGUAGE sql
STABLE
SET search_path = pg_catalog, synchro
BEGIN ATOMIC
SELECT scope_id FROM public.note_reviewers WHERE user_id = p_user_id;
END;
REVOKE ALL ON FUNCTION public.note_review_scopes(text) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.note_review_scopes(text) TO synchro_owner;
SELECT synchro.synchro_register_assignment_function('public.note_review_scopes', 100);
INSERT INTO public.note_reviewers (user_id, scope_id)
VALUES ('alice', 'notes:review');

The extension requires these function attributes and grants:

  • LANGUAGE sql with a BEGIN ATOMIC body.
  • Exactly one text argument. The argument is the canonical user ID.
  • RETURNS SETOF text.
  • STABLE, and not SECURITY DEFINER.
  • SET search_path = pg_catalog, synchro.
  • EXECUTE for synchro_owner and no EXECUTE for PUBLIC.
  • SELECT for the function owner on each relation that the function reads, and USAGE for the function owner on the schema of that relation.

The extension evaluates the function as its owner in a security-restricted operation. The function body never runs with the privileges of synchro_owner. synchro_quickstart_owner owns public.note_reviewers, so it needs no additional grant. Schema-qualify each relation in the function body. The second registration argument limits each user to 100 function scopes. A function result must not start with user:. A scope holds rows only when a membership function returns that scope for the rows.

Register the function again after you change its definition. Until then, connect and pull fail with 500 sync_integrity_failure. Remove the registration with this statement:

SELECT synchro.synchro_unregister_assignment_function();

The next successful connect or pull reconciles each assignment change. A change to public.note_reviewers needs no grant call. A changed set advances scope_set_version by one, and the same response reports the change. An added scope rebuilds. A removed scope causes local provenance cleanup. Rebuild does not reconcile assignment. A client cannot request an invented scope or grant itself one.

The adapter reads the canonical identity from the sub claim. This command creates a one-hour local token for alice and stores it only in the current shell environment:

Terminal window
export SYNCHRO_TUTORIAL_TOKEN="$(
python3 - <<'PY'
import base64
import hashlib
import hmac
import json
import os
import time
def part(value):
return base64.urlsafe_b64encode(
json.dumps(value, separators=(',', ':')).encode()
).rstrip(b'=')
head = part({'alg': 'HS256', 'typ': 'JWT'})
body = part({'sub': 'alice', 'exp': int(time.time()) + 3600})
message = head + b'.' + body
signature = hmac.new(
os.environ['JWT_SECRET'].encode(), message, hashlib.sha256
).digest()
print((message + b'.' + base64.urlsafe_b64encode(signature).rstrip(b'=')).decode())
PY
)"

Use this token only with the local tutorial client. Continue with First synchronization.

Stop the adapter, remove the temporary environment variables, and remove only the disposable tutorial database:

Terminal window
kill "$adapter_pid"
sudo sed -i "s/^synchro\.auto_start = on$/synchro.auto_start = off/" \
/etc/postgresql/18/synchro_tutorial/conf.d/synchro.conf
sudo pg_ctlcluster 18 synchro_tutorial restart
sudo -u postgres dropdb --host /var/run/postgresql --port 55432 synchro_quickstart
unset JWT_SECRET SYNCHRO_TUTORIAL_TOKEN DATABASE_URL LISTEN_ADDR MIN_CLIENT_VERSION
sudo pg_dropcluster 18 synchro_tutorial --stop

This removes only the disposable tutorial cluster. Do not use it for a shared cluster or a production database.