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