feat(firebase): initial stable release v0.9.0

Firebase App component with launcher lifecycle and health check integration.

What's included:
- Config with ProjectID (FIREBASE_PROJECT_ID env var); credentials via ADC
- Provider interface exposing native *firebase.App directly
- Component interface: launcher.Component + health.Checkable + Provider
- New(logger, cfg) constructor for lifecycle registration via lc.Append
- Health check via GetUser("health-probe-non-existent") + auth.IsUserNotFound at LevelCritical
- No-op OnStop (Firebase Admin SDK has no Close method)

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
This commit is contained in:
2026-03-19 13:35:40 +00:00
commit 3ef30c2354
13 changed files with 741 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
# ADR-001: SDK Health Check via Known-Nonexistent UID Probe
**Status:** Accepted
**Date:** 2026-03-18
## Context
The firebase-admin-go SDK wraps gRPC and HTTP internally. There is no explicit "ping" or
"connection check" method on the `firebase.App` or `auth.Client` types. Health checking
requires making a real API call that exercises the actual SDK communication path.
Two naive approaches have drawbacks:
1. **Check for a known real user**: Requires a stable test user to exist in production
Firebase. This is a maintenance burden and a security concern.
2. **String-match on the error message**: Coupling to SDK error message text is fragile;
internal messages change across SDK versions without notice.
## Decision
The health check calls `authClient.GetUser(ctx, "health-probe-non-existent")` with a UID
that is guaranteed not to exist. The expected outcome is a "user not found" error. The SDK
provides a typed predicate for this:
```go
_, err = authClient.GetUser(ctx, "health-probe-non-existent")
if err != nil {
if auth.IsUserNotFound(err) {
return nil // expected: the probe succeeded, service is reachable
}
return err // unexpected error: network failure, auth error, etc.
}
return nil
```
`auth.IsUserNotFound(err)` is an official SDK helper that inspects the error's underlying
type, not its message string. It is stable across SDK versions.
A "not found" response proves:
- The Firebase project is reachable.
- Authentication (ADC or service account key) is valid.
- The Auth service responded correctly.
Any other error (permission denied, network timeout, invalid project) is treated as a health
failure and propagated to the health framework.
## Consequences
**Positive:**
- The probe exercises the actual authentication and network path.
- `auth.IsUserNotFound` is a stable, typed check that does not depend on error messages.
- No real user is needed; the probe UID can never collide with a real account.
- The component is marked `health.LevelCritical` — if the probe fails, the service is
considered unhealthy.
**Negative:**
- Every health check invocation makes a live API call to Firebase. Under high-frequency
health polling, this generates traffic. Health check intervals should be configured
conservatively (e.g. every 30 seconds, not every second).
- The probe UID `"health-probe-non-existent"` is hardcoded. It is not configurable.

View File

@@ -0,0 +1,60 @@
# ADR-002: Firebase SDK Lifecycle Management via launcher.Component
**Status:** Accepted
**Date:** 2026-03-18
## Context
The firebase-admin-go SDK (`firebase.google.com/go/v4`) manages its own internal gRPC
connections, HTTP transports, and credential refresh cycles. The SDK entry point is a
`*firebase.App` instance, obtained via `fb.NewApp(ctx, config)`. Service-specific clients
(Auth, Firestore, etc.) are obtained from the App on demand via `app.Auth(ctx)` and
`app.Firestore(ctx)`.
Applications that construct the App outside of a lifecycle manager risk:
- Attempting to use the App before it is initialised.
- Not having a clear shutdown point (the SDK has no explicit `Close` method on `App`).
- Difficulty in testing components that depend on Firebase.
## Decision
`firebaseComponent` implements `launcher.Component` with three lifecycle methods:
- **`OnInit`**: Validates that `Config.ProjectID` is non-empty, then calls `fb.NewApp` to
create the App and store it in the struct. Returns an error if project ID is missing or
if the SDK fails to create the App. No network calls are made at this point — the SDK
is lazy about establishing connections.
- **`OnStart`**: Logs that the app is ready. Currently a no-op beyond logging; the SDK does
not require an explicit start call.
- **`OnStop`**: Logs shutdown. The firebase-admin-go SDK has no `Close` method on `*fb.App`.
Connections managed by the SDK (gRPC channels, HTTP transport) are closed by the Go
runtime's garbage collector when the App is no longer referenced. This is the accepted
behaviour of the SDK.
Consumers access the App via `App() *fb.App` (the `Provider` interface) and then obtain
service-specific clients themselves:
```go
authClient, err := component.App().Auth(ctx)
firestoreClient, err := component.App().Firestore(ctx)
```
This keeps the `firebase` module focused on App lifecycle without prescribing which Firebase
services consumers use.
## Consequences
**Positive:**
- Firebase App creation is integrated into the application startup sequence. Failures (bad
credentials, missing project ID) surface at startup, not at the first API call.
- The module is minimal: `OnInit` + `OnStart` + `OnStop` cover the full SDK lifecycle.
- Consumers are not limited to Auth; they can use any service the SDK supports.
**Negative:**
- There is no explicit SDK shutdown. The `OnStop` method is effectively a log-and-return.
In-flight requests to Firebase at shutdown time are handled by the SDK's own timeouts
and the context cancellation of callers, not by this module.
- `App()` returns `nil` before `OnInit` is called. Consumers must not call `App()` before
the lifecycle has been initialised (this is verified by `TestComponent_App_ReturnsNilBeforeInit`).