Summary
Add a happy-env db command group for sharing database snapshots (and other
per-project state) between teammates through a shared object store, with
store, list, and get as the three verbs.
Motivation
The README's "Not solved yet → Content" section names this gap: a project repo
is a wp-content directory, so there's no database in git and happy-env start
gives you an empty WordPress. That's fine for theme work and not much else.
Sharing a sanitized dump is the fastest way to make a fresh clone actually
runnable — a teammate stores a snapshot, another gets it, and both are on the
same content in seconds instead of hand-rolling an export/import each time.
This issue is scoped to the store/list/get idea only. Wiring a snapshot into
first-boot automatically (an afterInstall-style hook) is a natural companion
but is deliberately out of scope here.
Proposed commands
happy-env db store [--name <label>] [--ttl <duration>] # push a snapshot
happy-env db list # what's available for this project
happy-env db get [<name>] # pull a snapshot
store dumps the site's database, sanitizes it (see below), and uploads
it under a per-project prefix, e.g. projects/<project>/<name>/<timestamp>.sql.gz.
list shows what's stored for the current project: name, who stored it,
when, size, and remaining TTL.
get pulls a named snapshot (or the most recent) and imports it into the
local site.
The project is inferred from .happy-env.json, the same way the rest of the CLI
already resolves a project.
Authentication & identity
The load-bearing requirement is being able to answer who stored or pulled a
snapshot — a database is the most sensitive thing we handle, so access can't be a
shared, unattributable key.
- Browser login on demand. When a teammate runs
db store/db get without
valid credentials, prompt a browser-based sign-in (OAuth 2.0 authorization-code
flow with a loopback redirect — the same flow gcloud uses). No secret to
copy-paste.
- Longevity is via a refresh token, not a long-lived access token. Google
access tokens are capped at 1 hour and that isn't configurable. What makes it
feel like "stay logged in" is storing a refresh token and silently minting new
access tokens from it; the browser prompt only returns when the refresh token
is gone. The stored refresh token is a sensitive credential — it belongs in the
OS keychain, or at least ~/.happy-env/ at mode 0600.
- The org is the identity boundary. An Internal OAuth consent screen scoped
to the Happy Prime Google Workspace means only org accounts can complete the
login at all — that's the "verify who we are" check, for free and auditable
(access logs show the real person, not a shared key).
- Authentication vs authorization are distinct. Signing in proves who you
are; it does not grant bucket access. A teammate who is signed in but hasn't
been granted access should get a clear "you're signed in but not authorized"
message, not a generic failure.
Shortcut worth evaluating first: rather than implementing the OAuth loopback
flow, refresh, and secure storage ourselves, lean on Application Default
Credentials. If teammates run gcloud auth application-default login once, the
Google Cloud Storage client library picks those credentials up automatically and
we write ~no auth code — db store just detects a missing/expired credential and
points the user at that command. The trade is a gcloud dependency versus owning
a security-sensitive auth stack for a dev-convenience tool.
Storage backend
Two candidates:
- Google Cloud Storage — pairs directly with the Workspace identity story
above (per-user identity, IAM authorization, audit logs).
- Backblaze B2 — cheaper, but authenticates with application keys and has no
native per-user browser-identity story; we'd have to build one.
The identity requirement is itself the argument for GCS. Worth confirming during
implementation, but that's the leaning.
TTL / retention
--ttl on store exists so snapshots don't pile up, and it's a different TTL
from the auth one — this is object lifecycle, not sign-in duration.
- Store an
expires timestamp in object metadata and set a bucket lifecycle
rule as the backstop, so reaping doesn't depend on anyone running a command.
list can also surface/hide expired entries.
- On GCS this maps to object
customTime plus a lifecycle condition; a similar
bucket/prefix lifecycle rule exists on B2.
Naming & listing
- Naming is just the object key:
db store --name pre-migration →
projects/<project>/pre-migration/<timestamp>.sql.gz. Naming and per-project
namespacing are the same prefix mechanism.
- Listing is a prefix query over
projects/<project>/. Because identity is
established at store time, the list is attributable — each entry can show who
stored it.
Sanitization
Sanitization has to be part of store, not a follow-up. store is the moment a
database becomes a file in a shared bucket, so scrubbing (at minimum: user
emails, passwords, personal data, and any secrets in options) belongs in that
step by construction, not bolted on after the habit forms.
Acceptance criteria
Open questions
- GCS vs B2 — confirm the backend given the identity requirement.
- Build the OAuth flow directly, or piggyback on
gcloud ADC?
- What exactly counts as "sanitized," and is the ruleset shared across projects
or per-project?
- Does
store also cover uploads/media (a second, larger half of the same
problem), or is this DB-only to start?
Summary
Add a
happy-env dbcommand group for sharing database snapshots (and otherper-project state) between teammates through a shared object store, with
store,list, andgetas the three verbs.Motivation
The README's "Not solved yet → Content" section names this gap: a project repo
is a
wp-contentdirectory, so there's no database in git andhappy-env startgives you an empty WordPress. That's fine for theme work and not much else.
Sharing a sanitized dump is the fastest way to make a fresh clone actually
runnable — a teammate stores a snapshot, another gets it, and both are on the
same content in seconds instead of hand-rolling an export/import each time.
This issue is scoped to the store/list/get idea only. Wiring a snapshot into
first-boot automatically (an
afterInstall-style hook) is a natural companionbut is deliberately out of scope here.
Proposed commands
storedumps the site's database, sanitizes it (see below), and uploadsit under a per-project prefix, e.g.
projects/<project>/<name>/<timestamp>.sql.gz.listshows what's stored for the current project: name, who stored it,when, size, and remaining TTL.
getpulls a named snapshot (or the most recent) and imports it into thelocal site.
The project is inferred from
.happy-env.json, the same way the rest of the CLIalready resolves a project.
Authentication & identity
The load-bearing requirement is being able to answer who stored or pulled a
snapshot — a database is the most sensitive thing we handle, so access can't be a
shared, unattributable key.
db store/db getwithoutvalid credentials, prompt a browser-based sign-in (OAuth 2.0 authorization-code
flow with a loopback redirect — the same flow
gclouduses). No secret tocopy-paste.
access tokens are capped at 1 hour and that isn't configurable. What makes it
feel like "stay logged in" is storing a refresh token and silently minting new
access tokens from it; the browser prompt only returns when the refresh token
is gone. The stored refresh token is a sensitive credential — it belongs in the
OS keychain, or at least
~/.happy-env/at mode 0600.to the Happy Prime Google Workspace means only org accounts can complete the
login at all — that's the "verify who we are" check, for free and auditable
(access logs show the real person, not a shared key).
are; it does not grant bucket access. A teammate who is signed in but hasn't
been granted access should get a clear "you're signed in but not authorized"
message, not a generic failure.
Shortcut worth evaluating first: rather than implementing the OAuth loopback
flow, refresh, and secure storage ourselves, lean on Application Default
Credentials. If teammates run
gcloud auth application-default loginonce, theGoogle Cloud Storage client library picks those credentials up automatically and
we write ~no auth code —
db storejust detects a missing/expired credential andpoints the user at that command. The trade is a
gclouddependency versus owninga security-sensitive auth stack for a dev-convenience tool.
Storage backend
Two candidates:
above (per-user identity, IAM authorization, audit logs).
native per-user browser-identity story; we'd have to build one.
The identity requirement is itself the argument for GCS. Worth confirming during
implementation, but that's the leaning.
TTL / retention
--ttlonstoreexists so snapshots don't pile up, and it's a different TTLfrom the auth one — this is object lifecycle, not sign-in duration.
expirestimestamp in object metadata and set a bucket lifecyclerule as the backstop, so reaping doesn't depend on anyone running a command.
listcan also surface/hide expired entries.customTimeplus a lifecycle condition; a similarbucket/prefix lifecycle rule exists on B2.
Naming & listing
db store --name pre-migration→projects/<project>/pre-migration/<timestamp>.sql.gz. Naming and per-projectnamespacing are the same prefix mechanism.
projects/<project>/. Because identity isestablished at store time, the list is attributable — each entry can show who
stored it.
Sanitization
Sanitization has to be part of
store, not a follow-up.storeis the moment adatabase becomes a file in a shared bucket, so scrubbing (at minimum: user
emails, passwords, personal data, and any secrets in options) belongs in that
step by construction, not bolted on after the habit forms.
Acceptance criteria
happy-env db storedumps, sanitizes, and uploads a snapshot under aper-project prefix.
happy-env db listshows this project's snapshots with name, author, time,size, and remaining TTL.
happy-env db get [<name>]pulls and imports a snapshot (most recent whenno name is given).
store/gettriggers a browser sign-in; a signed-inbut unauthorized user gets a distinct, clear message.
stored refresh token is not world-readable.
--ttlcauses a snapshot to expire without anyone running a cleanupcommand.
store; a raw, unsanitized dump is neveruploaded.
Open questions
gcloudADC?or per-project?
storealso cover uploads/media (a second, larger half of the sameproblem), or is this DB-only to start?