Skip to content

Feature request: unified App handle for live + emulator backends #33

Description

@christiaan-lombard

Use case

I'm building a backend service that needs to support both live Firebase (production) and the Auth emulator (local dev/CI), selected at startup from environment config. The service holds a long-lived auth client and uses two App capabilities:

  1. auth() — user management (create_user, get_user, etc.)
  2. id_token_verifier() — verify incoming ID tokens on each request

In other ecosystems (e.g. TypeScript), this is straightforward: hold a reference to a single App interface and call the same methods regardless of backend. I'd like the Rust SDK to support a similar ergonomics story without every consumer re-implementing the same adapter layer.

Current friction

App<EmulatorCredentials> and App<AccessTokenCredentials> are separate types with different method signatures:

Method Live Emulator
auth() auth() auth(emulator_url: String)
id_token_verifier() Result<impl TokenValidator, _> impl TokenValidator (no Result)

This makes it impossible to write something as simple as:

fn build_client(app: ???) -> FirebaseAuthClient { ... }

without either generics (FirebaseAuthClient<C>) or a consumer-side enum.

Workaround we had to build

We ended up with ~70 lines of boilerplate in our app just to paper over the API differences:

pub enum FirebaseAppHandle {
    Live {
        app: App<AccessTokenCredentials>,
        project_id: String,  // duplicated — App::project_id is private
    },
    Emulated {
        app: App<EmulatorCredentials>,
        emulator_url: String,
    },
}

impl FirebaseAppHandle {
    pub fn auth(&self) -> FirebaseAuth<ReqwestApiClient> { /* match */ }

    pub fn id_token_validator(&self) -> Result<IdTokenValidator, AuthError> {
        match self {
            // Cannot use app.id_token_verifier() — see below
            Self::Live { project_id, .. } => Ok(IdTokenValidator::Live(
                LiveValidator::new_jwt_validator(project_id.clone())?,
            )),
            Self::Emulated { .. } => Ok(IdTokenValidator::Emulator(EmulatorValidator)),
        }
    }
}

pub enum IdTokenValidator {
    Live(LiveValidator),
    Emulator(EmulatorValidator),
}

Specific pain points

1. impl TokenValidator return type is not usable at call sites that need concrete types

App::id_token_verifier() returns impl TokenValidator, but our enum variant expects LiveValidator:

// Does not compile:
Ok(IdTokenValidator::Live(
    app.id_token_verifier()?.  // error: expected LiveValidator, found impl TokenValidator
))

We had to bypass App::id_token_verifier() entirely and call LiveValidator::new_jwt_validator() directly — which requires duplicating project_id because App::project_id is private.

2. Inconsistent error handling between live and emulator

Live id_token_verifier() returns Result<...>, emulator returns the validator directly. Consumers normalizing both paths need extra match arms for no functional reason.

3. Emulator URL is not part of App state

auth(emulator_url) requires the URL on every call (or the consumer must store it alongside App). For a long-lived client, the emulator host is configuration — it would be more natural to set it once at construction (e.g. from FIREBASE_AUTH_EMULATOR_HOST).

4. No shared abstraction for "live or emulated"

The phantom type parameter App<C> is a nice compile-time distinction, but most application code wants runtime selection. A first-party enum or trait would save every consumer from writing the same adapter.

Suggested improvements

Any of these would significantly improve the developer experience. Ordered roughly by impact:

Option A: First-party AppMode enum (highest impact)

pub enum App {
    Live { /* ... */ },
    Emulated { emulator_url: String, /* ... */ },
}

impl App {
    pub async fn live() -> Result<Self, ...> { ... }
    pub fn emulated(emulator_url: impl Into<String>) -> Self { ... }

    pub fn project_id(&self) -> &str { ... }
    pub fn auth(&self) -> FirebaseAuth<ReqwestApiClient> { ... }
    pub fn id_token_verifier(&self) -> Result<IdTokenVerifier, ...> { ... }
}

pub enum IdTokenVerifier {
    Live(LiveValidator),
    Emulator(EmulatorValidator),
}

impl TokenValidator for IdTokenVerifier { ... }

This gives consumers a single type to store and pass around, with normalized method signatures.

Option B: Normalize the existing impl blocks

Smaller changes that would still help:

  • Unify auth(): e.g. auth(&self) -> FirebaseAuth<...> on both variants, with emulator URL set at App::emulated(url) construction time.
  • Unify id_token_verifier(): same return type on both paths — Result<IdTokenVerifier, _> or return concrete enums instead of impl TokenValidator.
  • Expose project_id() as a public accessor on both App variants.
  • Return concrete types from id_token_verifier() (LiveValidator / EmulatorValidator) rather than impl TokenValidator, or provide an SDK-owned enum that implements TokenValidator.

Option C: Object-safe TokenValidator

If TokenValidator were object-safe (e.g. via async_trait or boxed futures), consumers could use Box<dyn TokenValidator>. Currently the impl Future return on validate() prevents this, forcing the concrete-type enum workaround.

Environment

  • rs-firebase-admin-sdk (latest from crates.io)
  • Rust 2024 edition
  • Use case: Axum HTTP server holding Arc<FirebaseAuthClient> across request handlers

Happy to contribute

If any of these directions align with your design goals, I'm happy to open a PR. Just let me know which approach you'd prefer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions