Ppoppo Docs

Map Ppoppo users to your database

When a user signs in, UserInfo returns their sub — the ppnum_id, an immutable ULID that is your stable key for that user. This guide shows a minimal schema and the find-or-create logic. For where sub comes from, see the OAuth2 PKCE guide.

Schema

Store ppnum_id on your user record, and keep the refresh token on the session, not the user:

CREATE TABLE users (
    id          VARCHAR(26) PRIMARY KEY,       -- your user id (ULID recommended)
    name        VARCHAR(100) NOT NULL,         -- your application fields
    ppnum_id    VARCHAR(26) NOT NULL UNIQUE,   -- the OAuth `sub` claim — the only stable link
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE user_sessions (
    id                        VARCHAR(26) PRIMARY KEY,
    user_id                   VARCHAR(26) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    refresh_token_ciphertext  BYTEA,           -- encrypted at rest, never plaintext
    expires_at                TIMESTAMPTZ NOT NULL,
    created_at                TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Store ppnum_id (and optionally ppnum). Never cache email — it's mutable and owned by PAS, so a cached copy drifts the moment the user changes it; fetch it from UserInfo when you need it. The refresh token belongs to a session (a logout CASCADE-deletes it) and must be encrypted at rest.

Handling the callback

Find-or-create by ppnum_id, then open a session:

async def handle_oauth_callback(code, state, code_verifier):
    tokens = await ppoppo.exchange_code(code, code_verifier)
    user   = await ppoppo.get_user_info(tokens.access_token)   # -> sub (ppnum_id), ppnum

    record = (await db.find_user_by_ppnum_id(user.sub)
              or await db.create_user(ppnum_id=user.sub))

    session = await db.create_session(
        user_id=record.id,
        refresh_token_ciphertext=encrypt(tokens.refresh_token),
    )
    set_cookie("session_id", session.id, http_only=True, secure=True)
    return record

That's the whole integration point: one stable identifier (ppnum_id), an encrypted session-scoped refresh token, and no cached PAS-owned data. See The ppnum model for the identifiers, and Trust boundary for the ownership rule.