Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 60 additions & 8 deletions docs/internal/postgres-flyio.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,17 +289,21 @@ const server = http.createServer(async (req, res) => {
}

// Generate token
// POST /token { "sub": "user-id", "role": "authenticated", "expiresIn": "24h" }
// POST /token { "sub": "user-id", "role": "rls_role", "expiresIn": "24h" }
if (req.method === "POST" && req.url === "/token") {
try {
const body = await parseBody(req);
const sub = body.sub || "anonymous";
const role = body.role || "authenticated";
const role = body.role;
const expiresIn = body.expiresIn || "24h";
const claims = body.claims || {};

if (!role) {
return respond(res, 400, { error: "role is required" });
}

const token = jwt.sign(
{ sub, role, aud: "authenticated", ...claims },
{ sub, role, ...claims },
JWT_SECRET,
{ expiresIn, algorithm: "HS256" }
);
Expand Down Expand Up @@ -403,17 +407,21 @@ const server = http.createServer(async (req, res) => {
return res.end(jwksResponse);
}

// POST /token { "sub": "user-id", "role": "authenticated", "expiresIn": "24h" }
// POST /token { "sub": "user-id", "role": "rls_role", "expiresIn": "24h" }
if (req.method === "POST" && req.url === "/token") {
try {
const body = await parseBody(req);
const sub = body.sub || "anonymous";
const role = body.role || "authenticated";
const role = body.role;
const expiresIn = body.expiresIn || "24h";
const claims = body.claims || {};

if (!role) {
return respond(res, 400, { error: "role is required" });
}

const token = jwt.sign(
{ sub, role, aud: "authenticated", iss: ISSUER, ...claims },
{ sub, role, iss: ISSUER, ...claims },
privateKey,
{ expiresIn, algorithm: "RS256", keyid: KID }
);
Expand Down Expand Up @@ -486,20 +494,64 @@ curl http://localhost:3002/.well-known/jwks.json

## Step 8: Generate a JWT token

Before generating JWTs for PostgreSQL, create the database role referenced by the token's `role` claim and grant it the permissions CloudSync needs.

### 8a. Create and grant the JWT role

Create the role:

```bash
cd /data/cloudsync-postgres
docker compose exec db psql -U postgres -d postgres -c "CREATE ROLE rls_role NOLOGIN;"
```

Grant schema and table permissions on current and future tables:

```bash
cd /data/cloudsync-postgres
docker compose exec db psql -U postgres -d test_database_1 -c "GRANT USAGE ON SCHEMA public TO rls_role;"
docker compose exec db psql -U postgres -d test_database_1 -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO rls_role;"
docker compose exec db psql -U postgres -d test_database_1 -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO rls_role;"
docker compose exec db psql -U postgres -d test_database_1 -c "GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO rls_role;"
docker compose exec db psql -U postgres -d test_database_1 -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO rls_role;"
```

Allow the connection-string user to switch into that role:

```bash
cd /data/cloudsync-postgres
docker compose exec db psql -U postgres -d postgres -c "GRANT rls_role TO postgres;"
```

Verify:

```bash
cd /data/cloudsync-postgres
docker compose exec db psql -U postgres -d postgres -c "SELECT rolname, rolsuper, rolcanlogin, rolbypassrls FROM pg_roles WHERE rolname = 'rls_role';"
docker compose exec db psql -U postgres -d test_database_1 -c "\\ddp"
```

If you want to test the exact session shape CloudSync uses:

```bash
cd /data/cloudsync-postgres
docker compose exec db psql -U postgres -d test_database_1 -c "BEGIN; SELECT set_config('request.jwt.claims', '{\"sub\":\"test-user-1\",\"role\":\"rls_role\"}', true); SET LOCAL ROLE rls_role; SELECT current_role, current_setting('request.jwt.claims', true); ROLLBACK;"
```

**HS256 (shared secret):**

```bash
curl -X POST http://localhost:3001/token \
-H "Content-Type: application/json" \
-d '{"sub": "user-1", "role": "authenticated"}'
-d '{"sub": "user-1", "role": "rls_role"}'
```

**RS256 (JWKS):**

```bash
curl -X POST http://localhost:3002/token \
-H "Content-Type: application/json" \
-d '{"sub": "user-1", "role": "authenticated"}'
-d '{"sub": "user-1", "role": "rls_role"}'
```

Response (both):
Expand Down
42 changes: 39 additions & 3 deletions docs/postgresql/quickstarts/postgres.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,27 @@ If the extension is installed correctly, PostgreSQL returns the CloudSync versio

---

## Upgrading CloudSync

Updating the Docker image or replacing `cloudsync.so` on disk does not by itself upgrade the SQL objects already registered in an existing database. PostgreSQL keeps the extension objects that were installed when `CREATE EXTENSION cloudsync;` was first run for that database.

When a release includes PostgreSQL extension upgrade scripts, the preferred upgrade path is:

```sql
ALTER EXTENSION cloudsync UPDATE;
```

If no upgrade path is available for the version you are moving from, a drop-and-recreate may be required to refresh the extension SQL objects:

```sql
DROP EXTENSION IF EXISTS cloudsync CASCADE;
CREATE EXTENSION cloudsync;
```

If you use JWT authentication with PostgreSQL RLS, recheck the database grants for the role named in your JWT `role` claim after upgrading. Recreating the extension also recreates extension-owned objects such as `cloudsync_changes`, so grants on those objects may need to be reapplied. See [PostgreSQL Role Requirement](../reference/jwt-claims.md#postgresql-role-requirement).

---

## Step 3: Register Your Database in the CloudSync Dashboard

In the [CloudSync dashboard](https://dashboard.sqlitecloud.io/), create a new workspace with the **PostgreSQL** provider, then add a project with your PostgreSQL connection string:
Expand All @@ -106,6 +127,8 @@ On the **Client Integration** tab you'll find your **Database ID** and authentic

The fastest way to test CloudSync without per-user access control — no JWT setup needed.

With API key authentication, CloudSync uses the database role resolved from the API-key-authenticated connection when available; otherwise it falls back to the role from the connection string.

```sql
SELECT cloudsync_network_init('<database-id>');
SELECT cloudsync_network_set_apikey('<username>:<password>');
Expand All @@ -116,9 +139,22 @@ SELECT cloudsync_network_sync();

1. Set **Row Level Security** to **Yes, enforce RLS**
2. Under **Authentication (JWT)**, click **Configure authentication** and choose:
- **HMAC Secret (HS256):** Enter your JWT secret (or generate one: `openssl rand -base64 32`)
- **JWKS Issuer Validation:** Enter the issuer base URL from your token's `iss` claim (e.g. `https://your-auth-domain`). CloudSync automatically fetches the JWKS document from `<issuer-url>/.well-known/jwks.json`
3. In your client code:
- **HMAC Secret (HS256):**
- Enter your JWT secret (or generate one: `openssl rand -base64 32`)
- Optionally add **Expected audiences**. When configured, a token's `aud` claim must contain at least one of the configured audience values.
- **JWKS Issuer Validation:**
- Enter the issuer base URL from your token's `iss` claim (for example `https://your-auth-domain`)
- By default, CloudSync uses OIDC discovery: it requests `<issuer>/.well-known/openid-configuration` and reads the returned `jwks_uri`
- Optionally set an **Explicit JWKS URI** to bypass OIDC discovery and use a specific JWKS endpoint directly. This must be a full HTTPS URI.
- Optionally add **Expected audiences**. When configured, a token's `aud` claim must contain at least one of the configured audience values.
3. CloudSync validates JWTs as follows:
- **HS256:** uses the configured JWT secret
- **JWKS:** uses the explicit `jwksUri` when provided; otherwise CloudSync requests `<issuer>/.well-known/openid-configuration` and reads `jwks_uri`
- CloudSync does not fall back directly to `<issuer>/.well-known/jwks.json` when discovery is used
4. For claim details and RLS examples, see:
- [JWT Claims Reference](../reference/jwt-claims.md)
- [RLS Reference](../reference/rls.md)
5. In your client code:
```sql
SELECT cloudsync_network_init('<database-id>');
SELECT cloudsync_network_set_token('<jwt-token>');
Expand Down
52 changes: 44 additions & 8 deletions docs/postgresql/quickstarts/supabase-self-hosted.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ Follow [Supabase's Installing Supabase](https://supabase.com/docs/guides/self-ho
```yaml
db:
# Supabase on PostgreSQL 15
image: sqlitecloud/sqlite-sync-supabase:15.8.1.085
image: sqlitecloud/sqlite-sync-supabase:15
# instead of: public.ecr.aws/supabase/postgres:15.8.1.085

# OR Supabase on PostgreSQL 17
image: sqlitecloud/sqlite-sync-supabase:17.6.1.071
image: sqlitecloud/sqlite-sync-supabase:17
# instead of: public.ecr.aws/supabase/postgres:17.6.1.071
```

Use the tag that matches your Supabase Postgres base image exactly. Convenience tags `sqlitecloud/sqlite-sync-supabase:15` and `sqlitecloud/sqlite-sync-supabase:17` are also published, but the exact Supabase tag is the safest choice.
Use the CloudSync image tag that matches your Supabase PostgreSQL major version. The published major tags `sqlitecloud/sqlite-sync-supabase:15` and `sqlitecloud/sqlite-sync-supabase:17` are the standard choice. Exact Supabase base-image tags may also be published for some releases, but they are optional and not required for normal setup.

### Add the CloudSync Init Script

Expand Down Expand Up @@ -59,8 +59,8 @@ Follow [Supabase's Updating](https://supabase.com/docs/guides/self-hosting/docke

```bash
# Update docker-compose.yml to use:
# sqlitecloud/sqlite-sync-supabase:15.8.1.085
# or sqlitecloud/sqlite-sync-supabase:17.6.1.071
# sqlitecloud/sqlite-sync-supabase:15
# or sqlitecloud/sqlite-sync-supabase:17
docker compose pull
docker compose down && docker compose up -d
```
Expand All @@ -83,6 +83,27 @@ If the extension is installed correctly, PostgreSQL returns the CloudSync versio

---

## Upgrading CloudSync

Updating the `sqlitecloud/sqlite-sync-supabase` image does not by itself upgrade the SQL objects already registered in an existing database. PostgreSQL keeps the extension objects that were installed when `CREATE EXTENSION cloudsync;` was first run for that database.

When a release includes PostgreSQL extension upgrade scripts, the preferred upgrade path is:

```sql
ALTER EXTENSION cloudsync UPDATE;
```

If no upgrade path is available for the version you are moving from, a drop-and-recreate may be required to refresh the extension SQL objects:

```sql
DROP EXTENSION IF EXISTS cloudsync CASCADE;
CREATE EXTENSION cloudsync;
```

If you use JWT authentication with PostgreSQL RLS, recheck the database grants for the role named in your JWT `role` claim after upgrading. Recreating the extension also recreates extension-owned objects such as `cloudsync_changes`, so grants on those objects may need to be reapplied. See [PostgreSQL Role Requirement](../reference/jwt-claims.md#postgresql-role-requirement).

---

## Step 3: Register Your Database in the CloudSync Dashboard

In the [CloudSync dashboard](https://dashboard.sqlitecloud.io/), create a new workspace with the **Supabase (Self-hosted)** provider, then add a project with your PostgreSQL connection string:
Expand All @@ -107,6 +128,8 @@ On the **Client Integration** tab you'll find your **Database ID** and authentic

The fastest way to test CloudSync without per-user access control — no JWT setup needed.

With API key authentication, CloudSync uses the database role resolved from the API-key-authenticated connection when available; otherwise it falls back to the role from the connection string.

```sql
SELECT cloudsync_network_init('<database-id>');
SELECT cloudsync_network_set_apikey('<username>:<password>');
Expand All @@ -117,9 +140,22 @@ SELECT cloudsync_network_sync();

1. Set **Row Level Security** to **Yes, enforce RLS**
2. Under **Authentication (JWT)**, click **Configure authentication** and choose:
- **HMAC Secret (HS256):** Enter your `JWT_SECRET` from Supabase's `.env`
- **JWKS Issuer Validation:** Enter the issuer base URL from your token's `iss` claim (e.g. `https://your-auth-domain`). CloudSync automatically fetches the JWKS document from `<issuer-url>/.well-known/jwks.json`
3. In your client code:
- **HMAC Secret (HS256):**
- Enter your `JWT_SECRET` from Supabase's `.env`
- Optionally add **Expected audiences**. When configured, a token's `aud` claim must contain at least one of the configured audience values.
- **JWKS Issuer Validation:**
- Enter the issuer base URL from your token's `iss` claim (for example `https://your-auth-domain`)
- By default, CloudSync uses OIDC discovery: it requests `<issuer>/.well-known/openid-configuration` and reads the returned `jwks_uri`
- Optionally set an **Explicit JWKS URI** to bypass OIDC discovery and use a specific JWKS endpoint directly. This must be a full HTTPS URI.
- Optionally add **Expected audiences**. When configured, a token's `aud` claim must contain at least one of the configured audience values.
3. CloudSync validates JWTs as follows:
- **HS256:** uses the configured JWT secret
- **JWKS:** uses the explicit `jwksUri` when provided; otherwise CloudSync requests `<issuer>/.well-known/openid-configuration` and reads `jwks_uri`
- CloudSync does not fall back directly to `<issuer>/.well-known/jwks.json` when discovery is used
4. For claim details and RLS examples, see:
- [JWT Claims Reference](../reference/jwt-claims.md)
- [RLS Reference](../reference/rls.md)
5. In your client code:
```sql
SELECT cloudsync_network_init('<database-id>');
SELECT cloudsync_network_set_token('<jwt-token>');
Expand Down
Loading
Loading