Skip to content

Add happy-env db store/list/get for sharing sanitized database snapshots #1

Description

@jeremyfelt

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

  • happy-env db store dumps, sanitizes, and uploads a snapshot under a
    per-project prefix.
  • happy-env db list shows this project's snapshots with name, author, time,
    size, and remaining TTL.
  • happy-env db get [<name>] pulls and imports a snapshot (most recent when
    no name is given).
  • An unauthenticated store/get triggers a browser sign-in; a signed-in
    but unauthorized user gets a distinct, clear message.
  • Credentials persist across runs without re-prompting each time, and the
    stored refresh token is not world-readable.
  • --ttl causes a snapshot to expire without anyone running a cleanup
    command.
  • Sanitization runs as part of store; a raw, unsanitized dump is never
    uploaded.

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    developmentFront or back-end development is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions