Use this guide to connect the Flutter client to Papyrus authentication and PowerSync.
- Offline mode is local-only and does not create a server session.
- Email/password auth uses Papyrus auth endpoints.
- Google auth uses a Papyrus-owned browser OAuth flow.
- The Flutter client stores Papyrus refresh tokens and keeps access tokens in memory.
- PowerSync receives short-lived JWTs from Papyrus.
- Local PowerSync writes upload through the Papyrus server.
- Flutter builds
PapyrusApiConfig.fromEnvironment(). AuthProvider.bootstrap()asksAuthRepositoryfor a stored refresh token.- With a stored refresh token, Flutter calls
POST /v1/auth/refresh. - After auth succeeds,
main.dartconnectsPapyrusPowerSyncService. - PowerSync calls
PapyrusPowerSyncConnector.fetchCredentials(). - The connector calls
POST /v1/auth/powersync-token. - PowerSync connects to
POWERSYNC_SERVICE_URLwith the Papyrus-issued JWT. - PowerSync reads user-scoped rows from Postgres through
server/powersync/sync-config.yaml. - Local PowerSync writes upload through
POST /v1/sync/powersync-upload. - The server validates ownership and writes the mutations to Postgres.
AuthProvider.setOfflineMode(true) clears Papyrus tokens and loads local data.
PowerSync disconnects and clears the synced local database. App routes stay
available through AuthProvider.isOfflineMode.
Login:
POST /v1/auth/login{
"email": "reader@example.com",
"password": "SecureP@ss123",
"client_type": "mobile",
"device_label": "flutter-android"
}Registration uses POST /v1/auth/register with the same client metadata and a
display_name.
Auth responses include:
access_token: Papyrus bearer token kept in memory.refresh_token: opaque token stored byTokenStore.expires_in: access token lifetime in seconds.user: authenticated user profile.
Flutter opens the Papyrus OAuth start URL:
GET /v1/auth/oauth/google/start?redirect_uri=<app-callback-uri>
Google returns to Papyrus:
GET /v1/auth/oauth/google/callback
Papyrus redirects back to the Flutter callback with a one-time code. Flutter exchanges that code for Papyrus tokens:
POST /v1/auth/exchange-code{
"code": "<papyrus-code>",
"client_type": "web",
"device_label": "flutter-web"
}Flutter callback URIs:
- Mobile:
papyrus://auth/callback - Linux and Windows:
http://localhost:43821/auth/callback - Web:
<current-origin>/auth/callback
Google Cloud redirect URI:
http://localhost:8080/v1/auth/oauth/google/callback
AuthRepository owns token refresh.
bootstrap()refreshes during startup with the stored refresh token.createPowerSyncToken()retries once after a Papyrus401.uploadPowerSyncBatch()retries once after a Papyrus401.- Successful refresh responses replace the stored refresh token.
- Failed refresh clears tokens and signs the user out.
Flutter requests a PowerSync JWT from Papyrus:
POST /v1/auth/powersync-token
Authorization: Bearer <papyrus-access-token>{
"token": "<powersync-jwt>",
"expires_in": 300
}Papyrus signs PowerSync tokens with RS256:
sub: Papyrus user id.aud:POWERSYNC_JWT_AUDIENCE.type:powersync.iat: issued-at timestamp.exp: expiration timestamp.kid:POWERSYNC_JWT_KEY_IDheader.
PowerSync validates tokens through:
GET /v1/auth/jwksPull:
- PowerSync validates the client JWT.
sync-config.yamlselects rows withWHERE owner_user_id::text = auth.user_id().- PowerSync sends user rows to the Flutter local PowerSync database.
- Flutter repositories watch the local PowerSync database.
DataStorereceives a read snapshot for existing feature providers; books are never independently persisted in memory.
Upload:
- Flutter writes to the local PowerSync database.
- PowerSync queues CRUD mutations.
PapyrusPowerSyncConnector.uploadData()reads queued transactions.- The connector posts the batch to
POST /v1/sync/powersync-upload. - The server applies the complete CRUD transaction atomically after ownership and field validation.
- PowerSync replication sends committed changes to connected clients.
The production sync contract currently contains only books. Additional
entities should be added as complete vertical slices: PostgreSQL model and
migration, sync stream, upload validation, client schema, repository, and
end-to-end tests.
Guest and authenticated libraries use separate local databases:
papyrus-guest.dbis local-only and persists until the user deletes it.papyrus-account.dbconnects to PowerSync and is cleared on logout or account switch.- Guest records are never merged into an authenticated account.
Run from client/app/:
flutter run -d chrome --web-hostname papyrus.localhost --web-port 3000 --dart-define-from-file=.dart_defines