diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 349cbcc5..02ffdb95 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -216,7 +216,7 @@ Start with action-oriented language: "Use [component] to..." rather than "The [c - Use relative links for internal pages: `/sync/streams/overview` - Update `docs.json` when adding, moving, or removing pages - Add redirects in `docs.json` for any moved content -- Check for broken links: `npx mintlify broken-links` +- Check for broken links: `npx mintlify broken-links`. Mintlify requires Node 20.17–24; if the default `node` is newer, prefix with `PATH="/opt/homebrew/opt/node@24/bin:$PATH"` - Never use absolute URLs for internal links ## Sync Streams and Sync Rules diff --git a/.claude/commands/lint-docs.md b/.claude/commands/lint-docs.md index 94f01882..a18ad186 100644 --- a/.claude/commands/lint-docs.md +++ b/.claude/commands/lint-docs.md @@ -1,11 +1,11 @@ --- -allowed-tools: Bash(npx mintlify *), Bash(vale *), Bash(git diff *) +allowed-tools: Bash(npx mintlify *), Bash(PATH=* npx mintlify *), Bash(vale *), Bash(git diff *) description: Run documentation linting checks — broken links, Vale prose style, and build validation. Use before publishing or merging documentation changes. --- Run all documentation linting checks and report any issues. -1. Run `npx mintlify broken-links` to find broken internal links +1. Run `npx mintlify broken-links` to find broken internal links. Mintlify requires Node 20.17–24; if the default `node` is 25 or newer, run it with an LTS Node instead: `PATH="/opt/homebrew/opt/node@24/bin:$PATH" npx mintlify broken-links` (install with `brew install node@24` if missing). 2. Run `git diff main --name-only -- '*.mdx'` to get the list of changed MDX files 3. For each changed file, run `vale ` to check prose style 4. Report all broken links and Vale violations, grouped by file diff --git a/sync/advanced/partitioned-tables.mdx b/sync/advanced/partitioned-tables.mdx index 3b5bf3f0..251d7b2b 100644 --- a/sync/advanced/partitioned-tables.mdx +++ b/sync/advanced/partitioned-tables.mdx @@ -31,16 +31,19 @@ To use the same queries and same output table name for each partition, use `%` f -The wildcard character can only be used as the last character in the table name. +The wildcard character can only be used as the last character in the table name. To match tables across multiple schemas instead, see [Wildcard Schemas](/sync/advanced/schemas-and-connections#wildcard-schemas-postgres). -When using wildcard table names, the original table suffix is available in the special `_table_suffix` column. This works the same way in both Sync Streams and Sync Rules: +When using wildcard table names, you can filter on the original table suffix. In Sync Streams, use the `table_suffix()` function, prefixed with the table name or alias from the `FROM` clause (requires PowerSync Service v1.24.0 or later). In legacy Sync Rules, the suffix is available as the special `_table_suffix` column instead: ```yaml + config: + edition: 3 + streams: active_todos: - query: SELECT * FROM "todos_%" AS todos WHERE _table_suffix != 'archived' + query: SELECT * FROM "todos_%" AS todos WHERE todos.table_suffix() != 'archived' ``` diff --git a/sync/advanced/schemas-and-connections.mdx b/sync/advanced/schemas-and-connections.mdx index e75daaa1..7c6633b3 100644 --- a/sync/advanced/schemas-and-connections.mdx +++ b/sync/advanced/schemas-and-connections.mdx @@ -1,6 +1,6 @@ --- title: "Schemas and Connections" -description: "Configure Postgres schema usage in Sync Streams/Rules queries, connect to high-availability replicas, and plan for multiple database connections." +description: "Configure Postgres schema usage in Sync Streams/Rules queries, including wildcard schemas for schema-per-tenant setups, and connect to high-availability replicas." --- ## Schemas (Postgres) @@ -12,6 +12,31 @@ When no schema is specified, the Postgres `public` schema is used for every quer SELECT * FROM "other"."assets" ``` +## Wildcard Schemas (Postgres) + + + Wildcard schemas require [Sync Streams](/sync/streams/overview) and PowerSync Service v1.24.0 or later. They are currently only supported for Postgres connections. + + +Use `%` as a wildcard in the schema name to match tables with the same name across multiple schemas. `"%"` matches every schema, and a prefix such as `"tenant_%"` matches every schema whose name starts with `tenant_`. The wildcard can only be the last character of the schema name. Postgres system schemas (`pg_*` and `information_schema`) are never matched. + +Combine a wildcard schema with the [`schema()` function](/sync/supported-sql#functions), which returns the schema each row was replicated from, to filter rows by schema. This supports schema-per-tenant databases (a single database with one identical schema per tenant): one stream covers every tenant schema, and each client syncs only its own tenant's data, resolved from a JWT claim. + +```yaml +config: + edition: 3 + +streams: + work_orders: + query: SELECT * FROM "%".work_orders WHERE work_orders.schema() = auth.parameter('tenant_schema') +``` + +In this example, rows are grouped into a bucket per schema, and each client syncs only the bucket matching the `tenant_schema` claim in its JWT. Rows from all matched schemas sync into a single client-side table, named after the table in the query (`work_orders` here). + + + Each matched table must be part of the [PowerSync publication](/configuration/source-db/setup#3-create-powersync-publication). Tables that are not in the publication are skipped. + + ## High Availability / Replicated Databases (Postgres) When the source Postgres database is replicated, for example with Amazon RDS Multi-AZ deployments, specify a single connection with multiple host endpoints. Each host endpoint will be tried in sequence, with the first available primary connection being used. diff --git a/sync/streams/examples.mdx b/sync/streams/examples.mdx index d946d776..ba903a90 100644 --- a/sync/streams/examples.mdx +++ b/sync/streams/examples.mdx @@ -40,6 +40,21 @@ sub.unsubscribe(); For more complex organization structures where users can belong to multiple organizations, see [Expanding JSON Arrays](/sync/streams/parameters#expanding-json-arrays). +### Schema-per-Tenant Data (Postgres) + +For multi-tenant Postgres databases with one identical schema per tenant, use a wildcard schema with the `schema()` function. A single stream covers every tenant schema, and each client syncs only the schema named by the `tenant_schema` claim in its JWT: + +```yaml +config: + edition: 3 + +streams: + work_orders: + query: SELECT * FROM "%".work_orders WHERE work_orders.schema() = auth.parameter('tenant_schema') +``` + +This requires PowerSync Service v1.24.0 or later. See [Wildcard Schemas](/sync/advanced/schemas-and-connections#wildcard-schemas-postgres) for requirements and details. + ### Role-Based Access When different users should see different data based on their role, use JWT claims to apply visibility rules. This keeps authorization logic on the server side where it's secure. diff --git a/sync/supported-sql.mdx b/sync/supported-sql.mdx index 272a0233..870880d3 100644 --- a/sync/supported-sql.mdx +++ b/sync/supported-sql.mdx @@ -245,6 +245,15 @@ Most functions are from [SQLite built-in functions](https://www.sqlite.org/lang_ - **[datetime(time-value, [modifier])](https://www.sqlite.org/lang_datefunc.html)** — Returns a time-value as a date and time string, in the format YYYY-MM-DD HH:MM:SS. If the specifier is "subsec", milliseconds are also included. If the modifier is "unixepoch", the argument is interpreted as a Unix timestamp. Both modifiers can be included: `datetime(timestamp, 'unixepoch', 'subsec')`. The time-value argument is required — this function cannot be used to get the current time. - **[uuid_blob(id)](https://sqlite.org/src/file/ext/misc/uuid.c)** — Convert a UUID string to bytes. + + These functions return metadata about the table a row comes from, rather than operating on a value. Prefix each call with the name or alias of a table in the query's `FROM` clause, the same way you would qualify a column reference: with `FROM "todos_%" AS todos`, write `todos.table_suffix()`. The functions take no arguments, and can appear in `WHERE` clauses, in selected columns, and inside subqueries. + + - **schema()** — Returns the schema the row was replicated from. Combined with a [wildcard schema](/sync/advanced/schemas-and-connections#wildcard-schemas-postgres), this allows filtering rows by their source schema, e.g. `SELECT * FROM "%".work_orders WHERE work_orders.schema() = auth.parameter('tenant_schema')`. + - **table_name()** — Returns the name of the table the row was replicated from. This is the source table's name, not the alias or the output table name. + - **table_suffix()** — Returns the part of the table name matched by the trailing `%` of a [wildcard table name](/sync/advanced/partitioned-tables). For example, with `FROM "todos_%" AS todos`, `todos.table_suffix()` returns `2024` for rows from the `todos_2024` table. On tables without a wildcard name, the result is always empty and the compiler reports a warning. + + Supported in Sync Streams only, with PowerSync Service v1.24.0 or later. + - **[ST_AsGeoJSON(geometry)](/client-sdks/advanced/gis-data-postgis)** — Convert [PostGIS](/client-sdks/advanced/gis-data-postgis) (in Postgres) geometry from WKB to GeoJSON. Combine with JSON operators to extract specific fields. - **[ST_AsText(geometry)](/client-sdks/advanced/gis-data-postgis)** — Convert [PostGIS](/client-sdks/advanced/gis-data-postgis) (in Postgres) geometry from WKB to Well-Known Text (WKT).