Repository navigation
Description
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:
auth()— user management (create_user,get_user, etc.)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 atApp::emulated(url)construction time. - Unify
id_token_verifier(): same return type on both paths —Result<IdTokenVerifier, _>or return concrete enums instead ofimpl TokenValidator. - Expose
project_id()as a public accessor on bothAppvariants. - Return concrete types from
id_token_verifier()(LiveValidator/EmulatorValidator) rather thanimpl TokenValidator, or provide an SDK-owned enum that implementsTokenValidator.
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.
Activity
expl commentedon Jun 20, 2026
Thank you for reporting this. The reason it is designed like this is because I personally do not deploy the exact same binary for both CI/CD and the actual usage. Instead I use feature flags to indicate emulation or not. But I do understand your use case.
Let me review the implementation and consider how to reduce the friction between emulator and live interfaces.
expl commentedon Jun 24, 2026
@christiaan-lombard please review #34 and tell me if this would solve your problem
christiaan-lombard commentedon Jun 26, 2026
@expl That looks perfect, thanks for considering this.
expl commentedon Jun 27, 2026
Merged and released under
5.0.0