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:
60
docs/adr/ADR-001-sdk-health-check-pattern.md
Normal file
60
docs/adr/ADR-001-sdk-health-check-pattern.md
Normal 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.
|
||||
60
docs/adr/ADR-002-sdk-lifecycle-management.md
Normal file
60
docs/adr/ADR-002-sdk-lifecycle-management.md
Normal 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`).
|
||||
Reference in New Issue
Block a user