Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

User Data Integration

This guide explains how to manage application-specific user data alongside the oauth2-passkey library.

Overview

The oauth2-passkey library manages authentication data (users, credentials, OAuth2 accounts) in its own tables. Your application typically needs additional user data such as profiles, preferences, or application-specific records.

Recommended approach: Create separate tables in your database linked by user_id.

Database Patterns

Pattern 1: One-to-One (User Profile)

Each user has exactly one profile record.

oauth2-passkey library          Your Application
+------------------+           +------------------+
|     users        |           |  user_profiles   |
+------------------+           +------------------+
| user_id (PK)     |<--------->| user_id (PK/FK)  |
| account          |           | display_name     |
| label            |           | bio              |
| ...              |           | avatar_url       |
+------------------+           | theme            |
                               +------------------+

See demo-profile for a complete example.

Pattern 2: One-to-Many (User Data)

Each user has multiple records (todos, posts, orders, etc.).

oauth2-passkey library          Your Application
+------------------+           +------------------+
|     users        |           |      todos       |
+------------------+           +------------------+
| user_id (PK)     |<----+     | id (PK)          |
| account          |     +---->| user_id (FK)     |
| label            |           | title            |
| ...              |           | completed        |
+------------------+           +------------------+

See demo-todo for a complete example.

Database Configuration

The library and your application can use any combination of databases.

Same Database

Both library and app share a single PostgreSQL database.

GENERIC_DATA_STORE_TYPE=postgresql
GENERIC_DATA_STORE_URL='postgres://demo:demo@localhost:5432/demo'
YOUR_APP_DATABASE_URL='postgres://demo:demo@localhost:5432/demo'

Benefits:

  • Foreign key constraints between users and your tables
  • Efficient JOINs across authentication and application data
  • Single database to manage and backup

Separate Databases

Library and app use independent databases.

GENERIC_DATA_STORE_TYPE=sqlite
GENERIC_DATA_STORE_URL='sqlite:/tmp/auth.db'
YOUR_APP_DATABASE_URL='postgres://demo:demo@localhost:5432/myapp'

Benefits:

  • Clear isolation between library and application data
  • Independent scaling and management
  • Flexibility to use different database systems

Implementation Guide

1. Define Your Schema

-- One-to-one: User profiles
CREATE TABLE user_profiles (
    user_id TEXT PRIMARY KEY,
    display_name TEXT,
    bio TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- One-to-many: User todos
CREATE TABLE todos (
    id SERIAL PRIMARY KEY,
    user_id TEXT NOT NULL,
    title TEXT NOT NULL,
    completed BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_todos_user_id ON todos(user_id);

2. Set Up Database Connection

Use standard Axum State<T> pattern for your application’s database:

use sqlx::PgPool;

#[derive(Clone)]
pub struct AppState {
    pub pool: PgPool,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize oauth2-passkey library (uses its own storage)
    oauth2_passkey_axum::init().await?;

    // Initialize your app's database
    let pool = PgPoolOptions::new()
        .connect(&std::env::var("YOUR_APP_DATABASE_URL")?)
        .await?;
    let state = AppState { pool };

    // Build routes
    let app = Router::new()
        .route("/", get(index))
        .with_state(state)
        .merge(oauth2_passkey_full_router());

    // ...
}

3. Access User ID in Handlers

Use the AuthUser extractor to get the authenticated user’s ID:

use oauth2_passkey_axum::AuthUser;

async fn get_profile(
    State(state): State<AppState>,
    user: AuthUser,  // Automatically extracts authenticated user
) -> Result<Response, StatusCode> {
    let profile = sqlx::query_as!(
        UserProfile,
        "SELECT * FROM user_profiles WHERE user_id = $1",
        user.id
    )
    .fetch_optional(&state.pool)
    .await?;

    // ...
}

4. Protect Routes

Use middleware to require authentication:

use oauth2_passkey_axum::is_authenticated_redirect;

pub fn protected_routes() -> Router<AppState> {
    Router::new()
        .route("/profile", get(show_profile).post(update_profile))
        .route_layer(from_fn(is_authenticated_redirect))
}

Accessing OAuth2 Account Data

The library stores OAuth2 account information including profile pictures. You can access this data:

use oauth2_passkey_axum::{list_accounts_core, UserId};

async fn get_google_avatar(user_id: &str) -> Option<String> {
    let user_id = UserId::new(user_id.to_string()).ok()?;
    let accounts = list_accounts_core(user_id).await.ok()?;

    accounts
        .into_iter()
        .find(|a| a.provider == "google")
        .and_then(|a| a.picture)
}

Example Applications

DemoDescription
demo-profileUser profile extension (settings, preferences)
demo-todoApp data linked to users (general pattern)