mixly / DEPLOY.md
hank-vibe's picture
docs: update Space references from solora-hq/Mixly to jay-hank/mixly
2caaa2b
|
Raw
History Blame Contribute Delete
9.58 kB
# Mixly on Hugging Face Spaces β€” deploy & operations
The Space (`jay-hank/mixly`) is a **Docker** host. Its `Dockerfile` clones the
**private** GitHub repo `phamdung2209/Mixly` at build time and runs the Next.js app.
Public URL: `https://jay-hank-mixly.hf.space`
> ⚠️ **Data isolation.** Mixly lives under the `jay-hank` HF account β€” a
> **different account** from `solora-hq`, which hosts **Tierly** and is LIVE with
> real merchant data. Mixly's `DATABASE_URL`, `S3_BUCKET`, `S3_ENDPOINT`, and Space
> name are a completely separate set of identifiers from Tierly's β€” never copy a
> value from Tierly's `DEPLOY.md` here. Concretely: DB path is `/data/mixly.db`
> (not `tierly.db`), bucket is `Mixly-storage` (not `Tierly-storage`) under the
> `jay-hank` namespace (not `solora-hq`), Space is `jay-hank/mixly` (not
> `solora-hq/Tierly`). `jay-hank` has no access to `solora-hq` at all, so pointing
> Mixly at any of Tierly's identifiers won't just risk corrupting or leaking live
> merchant data β€” it will most likely just fail outright.
---
## 0. First-time setup (do this once β€” the Space already exists)
The Space `jay-hank/mixly` has already been created (Docker SDK) on the
`jay-hank` HF account. What's left:
1. **Add the remote and push this repo**:
```
git remote add origin https://huggingface.co/spaces/jay-hank/mixly
git push -u origin main
```
The first build will fail (or crash-loop) until the secrets in Β§2 are set β€”
that's expected, HF just needs a repo to attach the build to.
2. **Create the S3 storage bucket**: HF β†’ your `jay-hank` account β†’ **Storage** β†’
**New bucket** β†’ name it exactly `Mixly-storage` (bare name, no slashes). Double
check it is not `Tierly-storage`. Read the risk note below first β€” Storage
Buckets may not be available on this account's plan.
3. **Set every secret in Β§2 below** (Space β†’ Settings β†’ Variables and secrets),
then Space β†’ Settings β†’ **Factory rebuild**.
> 🚧 **Risk: HF Storage Buckets may not be available on a free, non-Pro account.**
> `jay-hank` is a free, non-Pro account. This matters more than it sounds: the HF
> free tier has an **ephemeral** filesystem, so without a working S3 bucket the
> SQLite database is **destroyed on every rebuild, restart, or wake-from-sleep** β€”
> the app would silently lose every merchant's bundles, settings, and stats.
> Tierly has already suffered exactly this data-loss failure once, which is why
> the restore-on-boot mechanism (Β§4) exists. Before treating this deployment as
> production-ready, confirm that a bucket named `Mixly-storage` can actually be
> created under the `jay-hank` namespace and that `S3_ACCESS_KEY_ID` /
> `S3_SECRET_ACCESS_KEY` work against `https://s3.hf.co/jay-hank`. If HF storage
> buckets aren't available on this plan, the options are: enable HF Persistent
> Storage (paid), upgrade the account, or point the backup at a different
> S3-compatible provider (`src/lib/s3.ts` speaks plain S3, so any S3-compatible
> endpoint should work).
---
## 1. GitHub token (required β€” the repo is private)
Cloning a private repo inside the Docker build needs a token. Create a
**fine-grained Personal Access Token**, scoped to just this repo, read-only:
1. GitHub β†’ **Settings β†’ Developer settings β†’ Personal access tokens β†’ Fine-grained tokens β†’ Generate new**.
2. **Resource owner**: `phamdung2209`. **Repository access**: *Only select repositories* β†’ `Mixly`.
3. **Permissions β†’ Repository β†’ Contents: Read-only** (that's all it needs).
4. Expiration: 90 days (rotate on expiry). Generate β†’ copy the `github_pat_…`.
Then add it to the Space as a **build secret**:
- Space β†’ **Settings β†’ Variables and secrets β†’ New secret**
- Name: `GITHUB_TOKEN` Β· Value: the `github_pat_…`
> The Dockerfile reads it via `--mount=type=secret,id=GITHUB_TOKEN`, so it is
> **never baked into an image layer**.
---
## 2. Space secrets (runtime) β€” Settings β†’ Variables and secrets
πŸ”’ Paste values in the HF UI, never into code/git. **Secrets** (hidden) vs
**Variables** (visible) β€” put all credentials as *Secrets*.
### Required
| Name | Value / where from |
|---|---|
| `GITHUB_TOKEN` | build secret from Β§1 |
| `SHOPIFY_API_KEY` | Partner Dashboard β†’ Mixly β†’ API credentials |
| `SHOPIFY_API_SECRET` | β€³ (secret) |
| `NEXT_PUBLIC_SHOPIFY_API_KEY` | **same value as** `SHOPIFY_API_KEY` |
| `SHOPIFY_SCOPES` | `read_orders,read_products,write_discounts` (match `shopify.app.toml`) |
| `SHOPIFY_APP_URL` | `https://jay-hank-mixly.hf.space` |
| `SHOPIFY_API_VERSION` | `2026-07` |
| `DATABASE_URL` | `file:/data/mixly.db` (persistent mount β€” see Β§3) |
| `SESSION_ENCRYPTION_KEY` | `openssl rand -hex 32`. **Set once, never change** (rotating it forces every store to re-auth). |
| `HF_SPACE_ID` | `jay-hank/mixly` β€” the in-code default (`src/lib/ops/backup.ts`) is still `solora-hq/Mixly`, which is wrong for this deployment, so this must be set explicitly. |
### Ops Console + DB access (`/ops`, `/ops/studio`)
| Name | Value |
|---|---|
| `OPS_ALLOWLIST` | comma-separated emails allowed into `/ops` |
| `AUTH_SECRET` | `npx auth secret`. Keep stable (else your `/ops` sessions drop). |
| `AUTH_GOOGLE_ID` | Google Cloud Console β†’ OAuth client ID |
| `AUTH_GOOGLE_SECRET` | β€³ (redirect URI: `https://jay-hank-mixly.hf.space/api/auth/callback/google`) |
### DB backup (Β§4) β€” HF Storage Bucket via S3 API
| Name | Value |
|---|---|
| `S3_ENDPOINT` | `https://s3.hf.co/jay-hank` (gateway scoped to the namespace) |
| `S3_BUCKET` | `Mixly-storage` (bare bucket name β€” **not** `Tierly-storage`) |
| `S3_ACCESS_KEY_ID` | `HFAK…` β€” HF **Access Tokens** β†’ token (Write) β†’ dropdown β†’ **Generate S3 credentials** |
| `S3_SECRET_ACCESS_KEY` | β€³ (shown once) |
| `HF_TOKEN` | HF bearer token β€” used to rebuild/restart the Space from `/ops/backup` after a DB restore (Β§4), and by `scripts/sync-env.mjs` (Β§5), NOT the S3 creds |
### Optional (features degrade gracefully when unset)
`SENTRY_DSN`, `GITHUB_REPO` (overrides the clone source; the Dockerfile's
`ARG GITHUB_REPO` build arg already defaults to the right repo).
Mixly has **no** Crisp chat, QStash, or email (Resend) integration β€” unlike
Tierly, none of `NEXT_PUBLIC_CRISP_WEBSITE_ID`, `QSTASH_*`, `RESEND_API_KEY`,
`EMAIL_FROM`, or `CRON_SECRET` apply here; don't set them.
---
## 3. Persistent storage
Space β†’ **Settings β†’ Persistent storage** β†’ enable (Small is plenty). It mounts
at `/data`, which is where `DATABASE_URL=file:/data/mixly.db` points. Without it,
`/data` resets on every rebuild β€” which is exactly why Β§4 exists.
> Do **not** mount the Storage Bucket as a data volume. Backup talks to it over the
> S3 API (Β§4); a bucket *mount* is a schedule-time dependency and, if HF can't attach
> it, the Space fails with "Scheduling failure: unable to schedule".
---
## 4. Database backup / restore (HF Storage Bucket, S3 API)
The app backs the SQLite DB up to a **Storage Bucket over the S3 API** and restores
it on boot if `/data` is empty β€” so data survives a rebuild even on the free tier.
Network-only (no mount), so it never blocks Space scheduling.
- **What's backed up:** only the SQLite DB file (`/data/mixly.db`). It holds
everything stateful: Shopify sessions (tokens encrypted at rest), bundle offers,
bundle configs, shop profiles, settings. Migrations live in git; nothing else
needs backing up.
- **The one thing that is NOT in the bucket:** `SESSION_ENCRYPTION_KEY`. It lives
only in Space secrets. Keep it β€” a restored DB with a lost/rotated key can't
decrypt stored Shopify tokens (the app then just re-exchanges them, no data loss,
but avoid churn).
- **Bucket:** `jay-hank/Mixly-storage`. Objects: `latest.db` (overwritten in place β€”
no history growth) + `snapshots/mixly-YYYY-MM-DD.db` (daily, pruned after 30 days).
- **Schedule:** in-process β€” an initial backup ~1 min after boot, then hourly, plus a
debounced write after each DB change and a final flush on SIGTERM (see
`src/lib/backup.ts` + `src/lib/s3.ts`, started from `src/instrumentation.ts`).
- **Operator-driven backup/restore:** unlike a plain snapshot loop, Mixly ships an
`/ops/backup` panel (Google-authenticated, see Β§2) that can trigger an immediate
backup, upload a snapshot to restore from, and inspect it (table/row checks)
before committing. Restoring requires typing `overwrite` to confirm, snapshots
the *current* DB as a `pre-restore-…db` rollback key first, then calls
`restartSpace()` (needs `HF_TOKEN`) to restart the Space onto the restored file.
---
## 5. Deploy / rebuild
- **Redeploy the web app:** push to GitHub `main`, then in the Space use
**Settings β†’ Factory rebuild** (re-clones latest `main`).
- **Push local env vars as Space secrets:** `HF_TOKEN=… node scripts/sync-env.mjs
.env.production.local jay-hank/mixly` β€” reads `KEY=value` lines and pushes each
as a secret via the HF API. The script's built-in default Space is still
`solora-hq/Mixly` (wrong for this deployment), so always pass `jay-hank/mixly`
explicitly as the second arg. Changes apply on the next rebuild/restart.
- **Deploy the Shopify Function / extensions:** that is a *separate* target, run from
your machine (needs the Shopify CLI + Partner auth) β€” `npm run app:deploy`. The
container also runs this non-fatally during a Space build (see Dockerfile), gated
on the `SHOPIFY_CLI_PARTNERS_TOKEN` build secret.
> After changing scopes or the app URL, re-open the app in the store to re-consent.