Skip to content

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.

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.

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.

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.

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/connect
POST /sync/push
POST /sync/pull
POST /sync/rebuild

GET /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.

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_scope and synchro_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.