hank-vibe commited on
Commit
0984f9f
Β·
0 Parent(s):

Initial commit: Mixly HF Space deploy config

Browse files

Modelled on Host-Tierly. Adapted after verifying against Mixly's
actual source (scripts/restore-db.mjs, src/lib/backup.ts, src/lib/s3.ts,
src/instrumentation.ts, src/lib/env.ts, shopify.app.toml):
- DB path /data/mixly.db, bucket Mixly-storage, snapshot prefix mixly-
- Space solora-hq/Mixly, GitHub source phamdung2209/Mixly
- Kept Prisma Studio proxy (Mixly has src/lib/ops/studio-proxy.ts)
- Dropped Crisp build secret (no Crisp integration in Mixly)
- Dropped --config=production (only a default shopify.app.toml exists)
- Scopes/API version taken from Mixly's own shopify.app.toml

Files changed (4) hide show
  1. .gitattributes +35 -0
  2. DEPLOY.md +155 -0
  3. Dockerfile +96 -0
  4. README.md +22 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
DEPLOY.md ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mixly on Hugging Face Spaces β€” deploy & operations
2
+
3
+ The Space (`solora-hq/Mixly`) is a **Docker** host. Its `Dockerfile` clones the
4
+ **private** GitHub repo `phamdung2209/Mixly` at build time and runs the Next.js app.
5
+
6
+ Public URL: `https://solora-hq-mixly.hf.space`
7
+
8
+ > ⚠️ **Data isolation.** `solora-hq` also hosts **Tierly**, which is LIVE with real
9
+ > merchant data. Mixly's `DATABASE_URL`, `S3_BUCKET`, and Space name are a
10
+ > completely separate set of identifiers from Tierly's β€” never copy a value from
11
+ > Tierly's `DEPLOY.md` here. Concretely: DB path is `/data/mixly.db` (not
12
+ > `tierly.db`), bucket is `Mixly-storage` (not `Tierly-storage`), Space is
13
+ > `solora-hq/Mixly` (not `solora-hq/Tierly`). Pointing Mixly at any of Tierly's
14
+ > identifiers risks corrupting or leaking live merchant data.
15
+
16
+ ---
17
+
18
+ ## 0. First-time setup (do this once β€” the Space doesn't exist yet)
19
+
20
+ 1. **Create the Space**: https://huggingface.co/new-space β†’ Owner `solora-hq`,
21
+ Space name `Mixly`, **SDK: Docker**, visibility your choice, no template.
22
+ 2. **Add the remote and push this repo**:
23
+ ```
24
+ git remote add origin https://huggingface.co/spaces/solora-hq/Mixly
25
+ git push -u origin main
26
+ ```
27
+ The first build will fail (or crash-loop) until the secrets in Β§2 are set β€”
28
+ that's expected, HF just needs a repo to attach the build to.
29
+ 3. **Create the S3 storage bucket**: HF β†’ your `solora-hq` org β†’ **Storage** β†’
30
+ **New bucket** β†’ name it exactly `Mixly-storage` (bare name, no slashes). Double
31
+ check it is not `Tierly-storage`.
32
+ 4. **Set every secret in Β§2 below** (Space β†’ Settings β†’ Variables and secrets),
33
+ then Space β†’ Settings β†’ **Factory rebuild**.
34
+
35
+ ---
36
+
37
+ ## 1. GitHub token (required β€” the repo is private)
38
+
39
+ Cloning a private repo inside the Docker build needs a token. Create a
40
+ **fine-grained Personal Access Token**, scoped to just this repo, read-only:
41
+
42
+ 1. GitHub β†’ **Settings β†’ Developer settings β†’ Personal access tokens β†’ Fine-grained tokens β†’ Generate new**.
43
+ 2. **Resource owner**: `phamdung2209`. **Repository access**: *Only select repositories* β†’ `Mixly`.
44
+ 3. **Permissions β†’ Repository β†’ Contents: Read-only** (that's all it needs).
45
+ 4. Expiration: 90 days (rotate on expiry). Generate β†’ copy the `github_pat_…`.
46
+
47
+ Then add it to the Space as a **build secret**:
48
+ - Space β†’ **Settings β†’ Variables and secrets β†’ New secret**
49
+ - Name: `GITHUB_TOKEN` Β· Value: the `github_pat_…`
50
+
51
+ > The Dockerfile reads it via `--mount=type=secret,id=GITHUB_TOKEN`, so it is
52
+ > **never baked into an image layer**.
53
+
54
+ ---
55
+
56
+ ## 2. Space secrets (runtime) β€” Settings β†’ Variables and secrets
57
+
58
+ πŸ”’ Paste values in the HF UI, never into code/git. **Secrets** (hidden) vs
59
+ **Variables** (visible) β€” put all credentials as *Secrets*.
60
+
61
+ ### Required
62
+ | Name | Value / where from |
63
+ |---|---|
64
+ | `GITHUB_TOKEN` | build secret from Β§1 |
65
+ | `SHOPIFY_API_KEY` | Partner Dashboard β†’ Mixly β†’ API credentials |
66
+ | `SHOPIFY_API_SECRET` | β€³ (secret) |
67
+ | `NEXT_PUBLIC_SHOPIFY_API_KEY` | **same value as** `SHOPIFY_API_KEY` |
68
+ | `SHOPIFY_SCOPES` | `read_orders,read_products,write_discounts` (match `shopify.app.toml`) |
69
+ | `SHOPIFY_APP_URL` | `https://solora-hq-mixly.hf.space` |
70
+ | `SHOPIFY_API_VERSION` | `2026-07` |
71
+ | `DATABASE_URL` | `file:/data/mixly.db` (persistent mount β€” see Β§3) |
72
+ | `SESSION_ENCRYPTION_KEY` | `openssl rand -hex 32`. **Set once, never change** (rotating it forces every store to re-auth). |
73
+
74
+ ### Ops Console + DB access (`/ops`, `/ops/studio`)
75
+ | Name | Value |
76
+ |---|---|
77
+ | `OPS_ALLOWLIST` | comma-separated emails allowed into `/ops` |
78
+ | `AUTH_SECRET` | `npx auth secret`. Keep stable (else your `/ops` sessions drop). |
79
+ | `AUTH_GOOGLE_ID` | Google Cloud Console β†’ OAuth client ID |
80
+ | `AUTH_GOOGLE_SECRET` | β€³ (redirect URI: `https://solora-hq-mixly.hf.space/api/auth/callback/google`) |
81
+
82
+ ### DB backup (Β§4) β€” HF Storage Bucket via S3 API
83
+ | Name | Value |
84
+ |---|---|
85
+ | `S3_ENDPOINT` | `https://s3.hf.co/solora-hq` (gateway scoped to the namespace) |
86
+ | `S3_BUCKET` | `Mixly-storage` (bare bucket name β€” **not** `Tierly-storage`) |
87
+ | `S3_ACCESS_KEY_ID` | `HFAK…` β€” HF **Access Tokens** β†’ token (Write) β†’ dropdown β†’ **Generate S3 credentials** |
88
+ | `S3_SECRET_ACCESS_KEY` | β€³ (shown once) |
89
+ | `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 |
90
+
91
+ ### Optional (features degrade gracefully when unset)
92
+ `SENTRY_DSN`, `HF_SPACE_ID` (defaults to `solora-hq/Mixly` in code β€” only set if
93
+ the Space is ever renamed), `GITHUB_REPO` (overrides the clone source; the
94
+ Dockerfile's `ARG GITHUB_REPO` build arg already defaults to the right repo).
95
+
96
+ Mixly has **no** Crisp chat, QStash, or email (Resend) integration β€” unlike
97
+ Tierly, none of `NEXT_PUBLIC_CRISP_WEBSITE_ID`, `QSTASH_*`, `RESEND_API_KEY`,
98
+ `EMAIL_FROM`, or `CRON_SECRET` apply here; don't set them.
99
+
100
+ ---
101
+
102
+ ## 3. Persistent storage
103
+
104
+ Space β†’ **Settings β†’ Persistent storage** β†’ enable (Small is plenty). It mounts
105
+ at `/data`, which is where `DATABASE_URL=file:/data/mixly.db` points. Without it,
106
+ `/data` resets on every rebuild β€” which is exactly why Β§4 exists.
107
+
108
+ > Do **not** mount the Storage Bucket as a data volume. Backup talks to it over the
109
+ > S3 API (Β§4); a bucket *mount* is a schedule-time dependency and, if HF can't attach
110
+ > it, the Space fails with "Scheduling failure: unable to schedule".
111
+
112
+ ---
113
+
114
+ ## 4. Database backup / restore (HF Storage Bucket, S3 API)
115
+
116
+ The app backs the SQLite DB up to a **Storage Bucket over the S3 API** and restores
117
+ it on boot if `/data` is empty β€” so data survives a rebuild even on the free tier.
118
+ Network-only (no mount), so it never blocks Space scheduling.
119
+
120
+ - **What's backed up:** only the SQLite DB file (`/data/mixly.db`). It holds
121
+ everything stateful: Shopify sessions (tokens encrypted at rest), bundle offers,
122
+ bundle configs, shop profiles, settings. Migrations live in git; nothing else
123
+ needs backing up.
124
+ - **The one thing that is NOT in the bucket:** `SESSION_ENCRYPTION_KEY`. It lives
125
+ only in Space secrets. Keep it β€” a restored DB with a lost/rotated key can't
126
+ decrypt stored Shopify tokens (the app then just re-exchanges them, no data loss,
127
+ but avoid churn).
128
+ - **Bucket:** `solora-hq/Mixly-storage`. Objects: `latest.db` (overwritten in place β€”
129
+ no history growth) + `snapshots/mixly-YYYY-MM-DD.db` (daily, pruned after 30 days).
130
+ - **Schedule:** in-process β€” an initial backup ~1 min after boot, then hourly, plus a
131
+ debounced write after each DB change and a final flush on SIGTERM (see
132
+ `src/lib/backup.ts` + `src/lib/s3.ts`, started from `src/instrumentation.ts`).
133
+ - **Operator-driven backup/restore:** unlike a plain snapshot loop, Mixly ships an
134
+ `/ops/backup` panel (Google-authenticated, see Β§2) that can trigger an immediate
135
+ backup, upload a snapshot to restore from, and inspect it (table/row checks)
136
+ before committing. Restoring requires typing `overwrite` to confirm, snapshots
137
+ the *current* DB as a `pre-restore-…db` rollback key first, then calls
138
+ `restartSpace()` (needs `HF_TOKEN`) to restart the Space onto the restored file.
139
+
140
+ ---
141
+
142
+ ## 5. Deploy / rebuild
143
+
144
+ - **Redeploy the web app:** push to GitHub `main`, then in the Space use
145
+ **Settings β†’ Factory rebuild** (re-clones latest `main`).
146
+ - **Push local env vars as Space secrets:** `HF_TOKEN=… node scripts/sync-env.mjs
147
+ [.env.production.local] [solora-hq/Mixly]` (both args optional, those are the
148
+ defaults) β€” reads `KEY=value` lines and pushes each as a secret via the HF API.
149
+ Changes apply on the next rebuild/restart.
150
+ - **Deploy the Shopify Function / extensions:** that is a *separate* target, run from
151
+ your machine (needs the Shopify CLI + Partner auth) β€” `npm run app:deploy`. The
152
+ container also runs this non-fatally during a Space build (see Dockerfile), gated
153
+ on the `SHOPIFY_CLI_PARTNERS_TOKEN` build secret.
154
+
155
+ > After changing scopes or the app URL, re-open the app in the store to re-consent.
Dockerfile ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+ #
3
+ # Hugging Face Space (Docker SDK) for Mixly.
4
+ #
5
+ # The app source lives in the PRIVATE GitHub repo, not in this Space repo β€” this
6
+ # Dockerfile clones it at build time using the GITHUB_TOKEN *build secret*, builds
7
+ # the Next.js app, then runs it. Keeping the Space tiny means a rebuild always
8
+ # pulls the latest `main` from GitHub.
9
+ #
10
+ # The SQLite DB lives on the /data mount. On boot it is restored from the HF Storage
11
+ # Bucket (S3 API) if /data is empty, so data survives even without paid Persistent
12
+ # Storage (see scripts/restore-db.mjs).
13
+
14
+ # ---- builder ----
15
+ FROM node:24-slim AS builder
16
+ WORKDIR /app
17
+ RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ # Which repo/branch to deploy. Override GITHUB_REF (Space variable) to pin a release.
21
+ ARG GITHUB_REPO=github.com/phamdung2209/Mixly.git
22
+ ARG GITHUB_REF=main
23
+
24
+ # Clone with the token mounted as a build secret β€” it is never written into an
25
+ # image layer. Add GITHUB_TOKEN in Space β†’ Settings β†’ Secrets.
26
+ RUN --mount=type=secret,id=GITHUB_TOKEN,required=true \
27
+ git clone --depth 1 --branch "${GITHUB_REF}" \
28
+ "https://x-access-token:$(cat /run/secrets/GITHUB_TOKEN)@${GITHUB_REPO}" .
29
+
30
+ ENV NEXT_TELEMETRY_DISABLED=1
31
+ # Placeholder values ONLY so env validation passes during `next build` (some server
32
+ # modules read env at import). The app reads the REAL values from HF secrets at
33
+ # runtime; none of these are NEXT_PUBLIC, so nothing is baked into the output.
34
+ ENV SHOPIFY_API_KEY=build SHOPIFY_API_SECRET=build SHOPIFY_SCOPES=read_products \
35
+ SHOPIFY_APP_URL=https://build.invalid SHOPIFY_API_VERSION=2026-07 DATABASE_URL=file:/tmp/build.db
36
+ # NEXT_PUBLIC_* is inlined into the client bundle at BUILD time, so it must exist
37
+ # during `npm run build` β€” a runtime secret is too late. The Shopify API key is
38
+ # public (it ships in the browser), so mounting the secret just for the build is safe.
39
+ RUN --mount=type=secret,id=NEXT_PUBLIC_SHOPIFY_API_KEY \
40
+ export NEXT_PUBLIC_SHOPIFY_API_KEY="$(cat /run/secrets/NEXT_PUBLIC_SHOPIFY_API_KEY 2>/dev/null)" && \
41
+ npm ci && npx prisma generate && npm run build && mkdir -p public
42
+
43
+ # Deploy the Shopify Function + app config as part of the Space build, so ONE rebuild
44
+ # ships the web app AND the extensions together. Only runs on a real rebuild (not on
45
+ # restart/wake), so no version churn. NON-FATAL: a Shopify hiccup never breaks the web
46
+ # deploy. Needs the SHOPIFY_CLI_PARTNERS_TOKEN Space secret (else it just skips).
47
+ RUN --mount=type=secret,id=SHOPIFY_CLI_PARTNERS_TOKEN \
48
+ if [ -s /run/secrets/SHOPIFY_CLI_PARTNERS_TOKEN ]; then \
49
+ export SHOPIFY_CLI_PARTNERS_TOKEN="$(cat /run/secrets/SHOPIFY_CLI_PARTNERS_TOKEN)" CI=true; \
50
+ for d in extensions/*/; do [ -f "${d}package.json" ] && (cd "$d" && npm install --no-audit --no-fund || true); done; \
51
+ npx shopify app deploy --allow-updates || echo "WARN: shopify app deploy failed (non-fatal)"; \
52
+ else echo "SHOPIFY_CLI_PARTNERS_TOKEN not set β€” skipping Shopify deploy"; fi
53
+
54
+ # ---- runner ----
55
+ FROM node:24-slim AS runner
56
+ WORKDIR /app
57
+ ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000 HOSTNAME=0.0.0.0
58
+
59
+ # tini as PID 1: forwards SIGTERM to the app and reaps zombies. Without it (e.g.
60
+ # `npm start` as PID 1) npm swallows SIGTERM and never forwards it to the Node
61
+ # server, so the shutdown DB backup (src/lib/backup.ts) never runs on a rebuild.
62
+ RUN apt-get update && apt-get install -y --no-install-recommends tini \
63
+ && rm -rf /var/lib/apt/lists/*
64
+
65
+ # node:24-slim already ships a `node` user at UID 1000 (which HF mounts /data for) β€”
66
+ # reuse it; creating a second UID-1000 user fails with "UID 1000 is not unique".
67
+ COPY --from=builder --chown=node /app/node_modules ./node_modules
68
+ COPY --from=builder --chown=node /app/.next ./.next
69
+ COPY --from=builder --chown=node /app/public ./public
70
+ COPY --from=builder --chown=node /app/package.json ./package.json
71
+ COPY --from=builder --chown=node /app/next.config.ts ./next.config.ts
72
+ COPY --from=builder --chown=node /app/prisma ./prisma
73
+ COPY --from=builder --chown=node /app/prisma.config.ts ./prisma.config.ts
74
+ COPY --from=builder --chown=node /app/scripts ./scripts
75
+
76
+ # Make /data writable with OR without Persistent Storage. If enabled, HF mounts
77
+ # /data (uid 1000) over this. If not, this ephemeral dir is used and data still
78
+ # survives rebuilds via the S3 bucket restore on boot (scripts/restore-db.mjs).
79
+ RUN mkdir -p /data && chown node:node /data
80
+ USER node
81
+
82
+ EXPOSE 3000
83
+ # 1) restore the DB from the HF Storage Bucket (S3) if /data is empty, 2) apply migrations,
84
+ # 3) start. Restore MUST run before migrate β€” migrate would otherwise create an
85
+ # empty DB and the restore would think data already exists.
86
+ # `exec next` (NOT `npm start`) so the Node server itself becomes the process that
87
+ # receives SIGTERM on rebuild/sleep β€” that's what triggers the final DB backup
88
+ # (src/lib/backup.ts). npm would fork the server as a child and swallow the signal.
89
+ # tini (ENTRYPOINT) forwards the signal and reaps zombies.
90
+ ENTRYPOINT ["tini", "--"]
91
+ # Prisma Studio runs in the background on port 5555. NOTE: prisma studio v7 binds
92
+ # 0.0.0.0 (no hostname flag) β€” it is NOT localhost-only. It stays private because HF
93
+ # routes ONLY port 3000 as ingress and network-isolates the container; the DB is
94
+ # reached via the operator + same-origin gated route handlers (src/lib/ops/studio-proxy)
95
+ # at /ops/studio. Do NOT reuse this where 5555 is published (e.g. docker -p 5555:5555).
96
+ CMD ["sh", "-c", "node scripts/restore-db.mjs && node_modules/.bin/prisma migrate deploy && (node_modules/.bin/prisma studio --port 5555 --browser none &) && exec node_modules/.bin/next start"]
README.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Mixly
3
+ emoji: πŸ“¦
4
+ colorFrom: purple
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 3000
8
+ pinned: false
9
+ license: mit
10
+ short_description: Product bundles that discount what's already in the cart. No new SKUs, no inventory drift.
11
+ ---
12
+
13
+ # Mixly β€” Hugging Face Space
14
+
15
+ Docker host for the [Mixly](https://github.com/phamdung2209/Mixly) Shopify app.
16
+ This Space repo holds only the `Dockerfile`; it clones the app source from the
17
+ private GitHub repo at build time.
18
+
19
+ **Deploy / operations guide β†’ [`DEPLOY.md`](./DEPLOY.md)** (GitHub token, secrets,
20
+ persistent storage, DB backup/restore, rebuilds).
21
+
22
+ Config reference: https://huggingface.co/docs/hub/spaces-config-reference