# 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.