Auth Integration
Synchro receives one canonical user ID for each authenticated protocol request. It does not accept a user ID from a JSON request member. The identity binds the client, scopes, cursors, and write policy.
The adapter accepts exactly one authentication mode. Do not configure more than one mode.
Trusted upstream authentication
Section titled “Trusted upstream authentication”Use UserIDResolver when an upstream API already validates the request. The
resolver returns the product’s canonical internal user ID. Return
ErrAuthRequired when the request has no authenticated user.
handler := synchroapi.Routes(synchroapi.Config{ DB: db, UserIDResolver: synchroapi.RequestContextUserIDResolver,})Set the canonical identity with WithUserID only after the trusted upstream
authentication middleware completes. Never copy a user ID from a request body,
query parameter, or client header into that context.
synchrod-pg configures only direct JWT validation from environment variables.
Embed Routes in the trusted upstream server when you need UserIDResolver.
Transaction-local push context
Section titled “Transaction-local push context”An embedded host can configure PreparePush to supply trusted context to application triggers during canonical push.
The callback receives the bounded request context and the push transaction.
Use transaction-local settings only.
The callback must honor its context.
It must not perform application DML, commit, rollback, or change isolation.
Synchro does not interpret host setting names or values.
handler := synchroapi.Routes(synchroapi.Config{ DB: db, UserIDResolver: synchroapi.RequestContextUserIDResolver, PreparePush: func(ctx context.Context, tx *sql.Tx) error { _, err := tx.ExecContext(ctx, "SELECT pg_catalog.set_config('host.request_id', $1, true)", trustedRequestID(ctx), ) return err },})The host defines trustedRequestID and supplies its value through the trusted request context.
Do not add host context values to the sync request body or an authentication mode.
The adapter begins, prepares, and queries with one context bounded by DatabaseQueryTimeout.
It commits before it maps or writes the extension response.
Begin, preparation, query, and commit failures return no successful push response.
Unfinished transactions roll back.
Transaction-local settings end at commit or rollback.
Exact response replay remains unchanged.
Other routes do not invoke the callback.
An unset callback preserves the direct query path.
Adapter JWT authentication
Section titled “Adapter JWT authentication”Use one of these modes when the adapter validates bearer tokens:
| Mode | Config fields |
Accepted algorithms |
|---|---|---|
| Shared secret | JWTSecret |
HS256 |
| Key set | JWKSURL, JWKSContext |
RS256 or ES256 |
JWTSecret, JWKSURL, and UserIDResolver are mutually exclusive.
JWKSContext is required when JWKSURL is set. Keep that context active for
the handler lifetime. Cancel it during server shutdown. JWTUserClaim selects
the canonical user claim and defaults to sub.
The adapter checks the token signature and algorithm. It rejects a token with
a past exp or a future nbf. It does not require exp, and it does not check
aud or iss. The user claim must be a nonempty string. Every rejected token
returns 401 auth_required.
A key set refreshes every hour. When a token names an unknown kid, the
adapter also fetches the key set again, at most once in five minutes. Publish a
new key before the identity provider signs tokens with it.
jwksContext, cancelJWKS := context.WithCancel(context.Background())defer cancelJWKS()
handler := synchroapi.Routes(synchroapi.Config{ DB: db, JWKSURL: "https://identity.example/.well-known/jwks.json", JWKSContext: jwksContext, JWTUserClaim: "sub",})Store a shared secret outside source control. Generate it with a protected
secret manager or with openssl rand -hex 32, then export it only to the
adapter process environment.
Transport boundary
Section titled “Transport boundary”The standalone adapter serves HTTP. Bind it to a private loopback or private network address. Terminate public HTTPS at a reverse proxy before requests reach the adapter. Forward the original request only from that trusted proxy.
The four protocol routes require authentication:
POST /sync/connectPOST /sync/pushPOST /sync/pullPOST /sync/rebuildGET /ready, GET /sync/schema, and GET /sync/tables are unauthenticated.
Keep the metadata routes on an operator network. GET /ready returns only a
boolean readiness result. Do not add worker, registry, slot, row, or table
diagnostics to a public route.
Scope assignment
Section titled “Scope assignment”The server assigns scopes. Identity stays outside sync semantics. The adapter resolves the canonical user ID and passes that ID with the request body to the extension. The extension does not receive the token or its claims.
The server assigns scopes from four sources:
- The default private scope is
user:{canonical_user_id}. - Registered shared scopes apply to every active client.
- An operator can grant or revoke an existing scope for one user with
synchro_grant_user_scopeandsynchro_revoke_user_scope. - A registered assignment function returns scopes from application data.
The assignment function receives only the canonical user ID as its one text
argument. It does not receive a token, a claim, a header, or a request member.
Put product authorization in application tables that the function reads. See
scope modeling for the function rules.
The next successful connect or pull reconciles each assignment change. An added scope has a null cursor and rebuilds. A removed scope causes local provenance cleanup. A client cannot invent, grant, or retain an unassigned scope.
Pull reconciles the scope set after request validation. A changed set advances
scope_set_version by exactly one and appears in scope_updates of the same
response. A pull that fails validation writes no assignment state. Rebuild does
not reconcile assignment, and a rebuild of a revoked scope is rejected.