This repo can now support a full local auth test loop:
- email/password login
- refresh and logout flows
- verification and password-reset emails
- PowerSync token minting
- Google OAuth browser flow after you add Google credentials
For Flutter client integration guidance, see flutter-auth-integration.md.
For the self-hosted PowerSync sync sandbox and two-client validation workflow, see powersync-sandbox.md.
- Start local dependencies:
docker compose up -d database mailpit- Generate local PowerSync signing keys:
./scripts/generate_dev_powersync_keys.sh- Make sure
.envcontains the auth values shown in.env.example.
Recommended local values when the API runs on the host with uvicorn:
PUBLIC_BASE_URL=http://localhost:8080
EMAIL_DELIVERY_ENABLED=true
SMTP_HOST=127.0.0.1
SMTP_PORT=1025
SMTP_USE_TLS=false
SMTP_USE_SSL=false
SMTP_FROM_EMAIL=noreply@papyrus.local
SMTP_FROM_NAME=Papyrus
POWERSYNC_JWT_PRIVATE_KEY_FILE=.local/powersync/private.pem
POWERSYNC_JWT_PUBLIC_KEY_FILE=.local/powersync/public.pem
POWERSYNC_JWT_KEY_ID=papyrus-powersync-dev
POWERSYNC_JWT_AUDIENCE=powersync-devIf the API runs inside Docker instead of on the host, set SMTP_HOST=mailpit and POSTGRES_HOST=database.
- Apply migrations and run the API:
uv run alembic upgrade head
uv run uvicorn papyrus.main:app --reload- Run the dev-pages asset server for live TypeScript and SCSS reload:
npm --prefix frontend/dev-pages install
npm --prefix frontend/dev-pages run devIf you do not want to run Vite, build the sandbox assets once instead:
npm --prefix frontend/dev-pages run build- API index:
http://localhost:8080/ - Swagger UI:
http://localhost:8080/docs - ReDoc:
http://localhost:8080/redoc - Dev auth sandbox:
http://localhost:8080/__dev/auth-sandbox - Mailpit inbox UI:
http://localhost:8025
Mailpit is a local SMTP sink. No real mailbox is needed.
- Trigger
forgot passwordorresend verificationfrom the sandbox or API. - Open
http://localhost:8025to inspect the delivered messages. - For the opt-in smoke test, use any recipient address:
RUN_SMTP_SMOKE_TEST=true \
AUTH_SMOKE_EMAIL_RECIPIENT=smoke@papyrus.local \
uv run pytest tests/integration/test_auth_smoke.py -m auth_smoke -qPapyrus uses a server-owned browser OAuth flow. The Flutter app opens:
GET /v1/auth/oauth/google/start
Google redirects back to the server callback:
GET /v1/auth/oauth/google/callback
The server then redirects to your app callback URI with a one-time Papyrus exchange code.
Create an OAuth client with:
- Client type:
Web application - Redirect URI:
- local desktop testing:
http://localhost:8080/v1/auth/oauth/google/callback - public tunnel/device testing:
https://<your-public-host>/v1/auth/oauth/google/callback
- local desktop testing:
Authorized JavaScript origins are not required for this backend-owned redirect flow. If the Google UI requires one for localhost testing, use:
http://localhost:8080
Set the resulting values in .env:
GOOGLE_OAUTH_CLIENT_ID=...
GOOGLE_OAUTH_CLIENT_SECRET=...
PUBLIC_BASE_URL=http://localhost:8080For mobile-device testing or any device where the browser cannot reach your workstation as localhost, use a public HTTPS base URL and set PUBLIC_BASE_URL to that exact value.
- Desktop same-machine testing:
PUBLIC_BASE_URL=http://localhost:8080- Google redirect URI:
http://localhost:8080/v1/auth/oauth/google/callback
- Mobile emulator, physical phone, or shared test device:
- expose the backend through a public HTTPS URL
- set
PUBLIC_BASE_URL=https://<your-public-host> - Google redirect URI:
https://<your-public-host>/v1/auth/oauth/google/callback
The callback URI must match Google exactly, including scheme, host, port, path, and trailing slash behavior.
For development:
- keep the app in testing mode
- add your Google account under test users if Google requires it
Papyrus only requests basic identity scopes:
openidemailprofile
The Google smoke test now validates a live Papyrus session produced by a successful Google browser login.
Recommended workflow:
- Complete a real Google login in the auth sandbox.
- Copy the access token or refresh token from the sandbox.
- Run the smoke test against the running server.
Access-token-only mode:
RUN_GOOGLE_SMOKE_TEST=true \
AUTH_SMOKE_SERVER_BASE_URL=http://localhost:8080 \
AUTH_SMOKE_GOOGLE_ACCESS_TOKEN=<access-token-from-sandbox> \
uv run pytest tests/integration/test_auth_smoke.py -m auth_smoke -qRefresh-token mode is more durable and also validates token rotation:
RUN_GOOGLE_SMOKE_TEST=true \
AUTH_SMOKE_SERVER_BASE_URL=http://localhost:8080 \
AUTH_SMOKE_GOOGLE_REFRESH_TOKEN=<refresh-token-from-sandbox> \
uv run pytest tests/integration/test_auth_smoke.py -m auth_smoke -qIf both are provided, the test tries the access token first and falls back to refresh if the access token is expired.
Notes:
- refresh-token mode rotates the provided refresh token, so the old token will stop working after the test
- on success, the test prints
AUTH_SMOKE_ROTATED_REFRESH_TOKEN=...; use that value for the next manual run - if you only provide an access token, the test is non-destructive but depends on that token still being unexpired
AUTH_SMOKE_SERVER_BASE_URLdefaults toPUBLIC_BASE_URLif omitted