You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: document full grant surface for non-superuser RLS sync
Previous guidance in jwt-claims.md listed an incomplete minimum set
(schema + cloudsync_changes + user table + sequence). Missing grants
on internal objects — shadow tables, cloudsync_settings,
cloudsync_site_id, cloudsync_table_settings, cloudsync_schema_versions,
app_schema_version, and the cloudsync_site_id_id_seq sequence — cause
the per-PK savepoint to silently roll back writes while
cloudsync_payload_apply still returns a non-zero column-change count,
so callers see success while rows never land.
jwt-claims.md: expand "PostgreSQL Role Requirement" with role creation
(NOLOGIN + GRANT), a recommended default-privileges pattern,
an explicit minimum allowlist for audited deployments, and a
BYPASSRLS service-role recipe.
rls.md: add a matching troubleshooting entry for the silent-skip
failure mode, linking back to the grants reference.
Copy file name to clipboardExpand all lines: docs/postgresql/reference/jwt-claims.md
+86-13Lines changed: 86 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -62,36 +62,109 @@ For PostgreSQL JWT authentication, the `role` claim must name a real database ro
62
62
That role should:
63
63
64
64
- already exist in PostgreSQL
65
-
- have the schema, table, and sequence privileges your sync operations need
66
-
- have access to the `cloudsync_changes` view used by PostgreSQL sync operations
65
+
- have the schema, table, and sequence privileges your sync operations need (see [Required Grants](#required-grants))
67
66
- be grantable by the connection-string user
68
67
69
68
If the JWT contains a `role` that does not exist, or the connection user cannot switch into it, PostgreSQL sync operations will fail even if the JWT itself is otherwise valid.
70
69
71
-
### Minimum Grants for a JWT Role
70
+
### Creating the Role
72
71
73
-
In a standard PostgreSQL setup, functions created by `CREATE EXTENSION cloudsync;` are executable by `PUBLIC` unless your cluster has been hardened with explicit `REVOKE EXECUTE` statements. In the normal case, the JWT role needs grants on:
72
+
A typical setup uses a `NOLOGIN` role that your connection user enters via `SET LOCAL ROLE` after JWT verification:
74
73
75
-
- the schema that contains your synced tables
76
-
- the `cloudsync_changes` view
77
-
- the synced user tables
78
-
- any sequences used by those tables
74
+
```sql
75
+
CREATE ROLE rls_role NOLOGIN;
76
+
77
+
-- Allow the connection-string user (e.g. `postgres`) to switch into it
78
+
GRANT rls_role TO postgres;
79
+
```
80
+
81
+
### Required Grants
82
+
83
+
`cloudsync_payload_apply` running as a non-superuser touches several internal CloudSync objects during apply — not just your user table. If any grant is missing on an internal object, the per-PK savepoint silently rolls back the write and the caller sees a non-zero column-change count with no rows landing (see [RLS Troubleshooting](./rls.md#apply-reports-a-count-but-rows-are-missing)).
84
+
85
+
There are two equivalent ways to configure this: the **recommended default-privileges pattern** (future-proof) or the **explicit minimum grant set** (tighter, for audited deployments).
86
+
87
+
#### Recommended: default-privileges pattern
88
+
89
+
Run this **before**`CREATE EXTENSION cloudsync`, as the role that will install the extension (typically `postgres`). Objects created afterwards — including all CloudSync internal tables and future `cloudsync_init` shadows — inherit the grants automatically:
GRANT USAGE, SELECT, UPDATEON SEQUENCES TO rls_role;
101
+
102
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
103
+
GRANT EXECUTE ON FUNCTIONS TO rls_role;
104
+
105
+
CREATE EXTENSION IF NOT EXISTS cloudsync;
106
+
```
107
+
108
+
**If the extension is already installed**, `ALTER DEFAULT PRIVILEGES` doesn't apply retroactively — backfill existing objects with a one-time broad grant, then still set defaults for future creations:
109
+
110
+
```sql
111
+
GRANTSELECT, INSERT, UPDATE, DELETEON ALL TABLES IN SCHEMA public TO rls_role;
112
+
GRANT USAGE, SELECTON ALL SEQUENCES IN SCHEMA public TO rls_role;
113
+
-- (plus the ALTER DEFAULT PRIVILEGES block above)
114
+
```
115
+
116
+
#### Explicit minimum grant set
79
117
80
-
Example:
118
+
For audited deployments that need an explicit allowlist, the tightest set that allows `cloudsync_payload_apply` to work under a non-superuser:
81
119
82
120
```sql
83
121
GRANT USAGE ON SCHEMA public TO rls_role;
122
+
GRANT USAGE ON SCHEMA auth TO rls_role;
123
+
124
+
-- User table (RLS policies filter rows within these grants)
125
+
GRANTSELECT, INSERT, UPDATE, DELETEON your_table TO rls_role;
126
+
127
+
-- Per-table CRDT shadow (created by cloudsync_init)
128
+
GRANTSELECT, INSERT, UPDATE, DELETEON your_table_cloudsync TO rls_role;
84
129
130
+
-- CloudSync metadata tables
131
+
GRANTSELECT, INSERT, UPDATE, DELETEON
132
+
cloudsync_settings,
133
+
cloudsync_table_settings,
134
+
cloudsync_site_id,
135
+
cloudsync_schema_versions,
136
+
app_schema_version
137
+
TO rls_role;
138
+
139
+
-- cloudsync_changes view: SELECT for apply-path readback, INSERT for the
140
+
-- INSTEAD OF trigger that feeds column changes into the flush buffer
85
141
GRANTSELECT, INSERT ON cloudsync_changes TO rls_role;
86
142
87
-
GRANTSELECT, INSERT, UPDATE, DELETEON TABLE your_table TO rls_role;
143
+
-- BIGSERIAL-backed sequence on cloudsync_site_id.id (nextval needs USAGE)
144
+
GRANT USAGE ON SEQUENCE cloudsync_site_id_id_seq TO rls_role;
88
145
89
-
GRANT USAGE, SELECTON SEQUENCE your_table_id_seq TO rls_role;
146
+
-- Your user table's sequence, if it uses SERIAL / IDENTITY
147
+
-- GRANT USAGE, SELECT ON SEQUENCE your_table_id_seq TO rls_role;
90
148
```
91
149
92
-
Administrative functions such as `cloudsync_init`, `cloudsync_enable`, `cloudsync_set*`, `cloudsync_terminate`, `cloudsync_cleanup`, `cloudsync_begin_alter`, and `cloudsync_commit_alter` should be run by the database owner during setup, not by client JWT roles.
150
+
Notes on the minimum set:
151
+
152
+
-**No `EXECUTE` grants on `cloudsync_*` functions or `auth.uid()` are required**, because PostgreSQL defaults `CREATE FUNCTION` to `EXECUTE TO PUBLIC`. If your cluster has revoked PUBLIC execute, grant `EXECUTE` explicitly on `cloudsync_payload_apply`, `cloudsync_payload_encode`, `cloudsync_changes_select`, `cloudsync_changes_insert_trigger`, `cloudsync_siteid`, `cloudsync_pk_encode`, and `cloudsync_encode_value`.
153
+
-**`app_schema_version` is not `cloudsync_*`-prefixed** — easy to miss in `cloudsync_%`-pattern grants.
154
+
-**Per-table shadows follow the `<table>_cloudsync` convention** — repeat the DML grant for every table passed to `cloudsync_init`.
155
+
-**Administrative functions** such as `cloudsync_init`, `cloudsync_enable`, `cloudsync_set*`, `cloudsync_terminate`, `cloudsync_cleanup`, `cloudsync_begin_alter`, and `cloudsync_commit_alter` should be run by the database owner during setup, not by client JWT roles.
156
+
- The minimum set will need widening if a future CloudSync version adds new internal objects. The default-privileges pattern above is future-proof.
157
+
158
+
### Service Role (RLS Bypass)
159
+
160
+
For server-side workers that need to apply payloads without RLS enforcement (admin restores, cross-user sync, maintenance jobs), create a dedicated role with `BYPASSRLS`:
161
+
162
+
```sql
163
+
CREATE ROLE service_role NOLOGIN BYPASSRLS;
164
+
GRANT service_role TO postgres;
165
+
```
93
166
94
-
If your PostgreSQL setup has revoked the default `PUBLIC` execute privileges on functions, you must also explicitly grant execute permissions on the specific CloudSync functions needed by your sync path.
167
+
Apply the same grants as for `rls_role`. Use this role only from trusted server code, never from JWT-gated request paths.
Copy file name to clipboardExpand all lines: docs/postgresql/reference/rls.md
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -178,6 +178,14 @@ When using Supabase:
178
178
- The `user_id` column in the synced data matches `auth.uid()`
179
179
- RLS policies reference the correct ownership column
180
180
181
+
### Apply reports a count, but rows are missing
182
+
183
+
**Symptom**: `cloudsync_payload_apply` returns a non-zero column-change count, but `SELECT` on the target table shows no new rows. No error is raised to the caller.
184
+
185
+
**Cause**: The calling role is missing a grant on one of CloudSync's internal objects — the per-table shadow (`<table>_cloudsync`), a metadata table (`cloudsync_settings`, `cloudsync_site_id`, `cloudsync_table_settings`, `cloudsync_schema_versions`, `app_schema_version`), the `cloudsync_changes` view, or the `cloudsync_site_id_id_seq` sequence. The per-PK savepoint rolls the write back, but `cloudsync_payload_apply` still returns the number of column changes it processed.
186
+
187
+
**Solution**: Apply the full grant set from [JWT Claims → Required Grants](./jwt-claims.md#required-grants). To pinpoint which object is missing, re-run the apply as a superuser or raise log verbosity and inspect the server log for `permission denied` entries preceded by the `cloudsync_payload_apply` call.
0 commit comments