# Install — Shopify Backup v2

A standalone, self-hosted, multi-user Shopify backup & restore app. Anyone can install. Sign-up with email + password, install the app on as many Shopify stores as you want, share access with teammates.

---

## What you get

- Email/password signup (first user = admin, optional `ADMIN_EMAIL` override)
- Per-user store access (admin sees all stores; regular users only see what they install or were granted)
- One-click Shopify install for any `*.myshopify.com` store
- **Re-authorize** button to refresh the token / re-grant scopes without re-adding the store
- Full backup: themes, content (pages/blogs/redirects/menus/policies/metaobjects/translations), catalog (products/collections), Files CDN, customers + orders (AES-256-GCM encrypted at rest)
- Safe non-destructive **restore** (dry-run → confirm). Cross-store **clone** supported.
- Scheduled daily backups (systemd timer) + on-demand
- "App setup" tab in the UI that shows the exact App URL / Redirect URL / Scopes to paste into Shopify

---

## Prerequisites

- A server with Docker + docker-compose (or just Node 18+)
- A domain + TLS (Let's Encrypt, Cloudflare, or behind any reverse proxy)
- A Shopify Partner account (free) to create the app

---

## 1. Create the Shopify app

Two options.

### Option A — Shopify Partner Dashboard (recommended for SaaS)

1. Go to <https://partners.shopify.com/> → **Apps** → **Create app** → **Create app manually**.
2. Fill in:
   - **App name:** `Shopify Backup` (or whatever)
   - **App URL:** `https://YOUR-DOMAIN/admin`
   - **Allowed redirection URL(s):** `https://YOUR-DOMAIN/api/auth/callback`
3. Open **API access** → copy **Client ID** and **Client secret**.
4. In **Configuration** → **App setup** → make sure **"Embedded in Shopify admin"** is **OFF**. (Embedded breaks the OAuth callback for this app.)
5. (Optional) Add a Webhook → Mandatory compliance topics (GDPR) pointing to `/api/webhooks/gdpr` — only needed if you list the app publicly.

### Option B — Custom App on a single store (fastest for personal use)

1. Store admin → **Settings → Apps and sales channels → Develop apps → Create an app**.
2. **Configuration → Admin API integration**:
   - **Allowed redirection URL(s):** `https://YOUR-DOMAIN/api/auth/callback`
   - **Scopes:** copy from `SCOPES.md` (or from the "App setup" tab in the UI after install)
3. **Install app** → copy the **Admin API access token** (`shpat_…`) — you can paste this instead of doing OAuth.
4. **API credentials** → copy the **API key** (= client_id) and **API secret key** (= client_secret).

---

## 2. Run with Docker (recommended)

```bash
git clone https://github.com/YOU/shopify-backup.git
cd shopify-backup
cp .env.example .env

# Generate secrets
echo "APP_SECRET=$(openssl rand -hex 32)" >> .env
echo "BACKUP_ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env

# Edit .env — fill in:
#   PUBLIC_URL=https://your-domain.com
#   SHOPIFY_CLIENT_ID=<from step 1>
#   SHOPIFY_CLIENT_SECRET=<from step 1>
#   (optional) ADMIN_EMAIL=you@example.com  ← becomes admin on first signup

docker compose up -d
```

The app is now listening on `http://127.0.0.1:3011`. Put it behind nginx / Caddy / Cloudflare Tunnel to terminate TLS at your domain.

### nginx (with Let's Encrypt)

```nginx
server {
  listen 443 ssl http2;
  server_name backup.example.com;

  ssl_certificate     /etc/letsencrypt/live/backup.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/backup.example.com/privkey.pem;

  client_max_body_size 50m;

  location / {
    proxy_pass http://127.0.0.1:3011;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;          # streams big backup zips
  }
}
```

---

## 3. First login

1. Open `https://YOUR-DOMAIN/admin`
2. Click **"Need an account? Create one"** → register with the email matching `ADMIN_EMAIL` (or any email if `ADMIN_EMAIL` is unset — the first signup becomes admin automatically).
3. Go to the **App setup** tab — verify the App URL / Redirect URL / Scopes match what you put into Shopify.
4. Go to **Stores** tab → enter your `myshopify.com` domain → click **Install on Shopify →**. Approve the scopes in the Shopify popup. Done.

---

## 4. Schedule daily backups (host install)

If you installed without Docker, you can use the bundled systemd units:

```bash
sudo cp systemd/shopify-backup-cron.service /etc/systemd/system/
sudo cp systemd/shopify-backup-cron.timer   /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now shopify-backup-cron.timer
systemctl list-timers shopify-backup-cron.timer    # verify
```

(In Docker, just add a `cron` sidecar or call `npm run backup:all` from your host cron pointing into the container.)

---

## 5. Promote / invite teammates

- **Admin view → Users tab** → see all registered users
- Click **Manage access** on a user → grant `viewer` / `editor` / `owner` role on any store
- Click **Promote** to make someone else an admin
- Click **Delete** to remove a user (their `store_access` grants are revoked; stores they own become orphan — admin still sees them)

Lock down signup after onboarding:

```bash
echo "DISABLE_SIGNUP=1" >> .env
docker compose restart
```

---

## 6. Re-authorize a store

If Shopify revokes the token (uninstall + reinstall, scope change, security event) the store row flips to `needs_reauth` and the UI shows a yellow **🔑 Re-authorize** button. Click it → you go through OAuth again → new token saved → done. The same button is always present as the `🔄` icon on each store row for proactive token refresh.

---

## 7. Update

```bash
cd shopify-backup
git pull
docker compose build --no-cache
docker compose up -d
```

The DB migration is idempotent — existing v1 installs upgrade in place. The single legacy `ADMIN_PASSWORD` keeps working until you create your first email account (the legacy login auto-promotes itself to user id=1 / admin on first successful login).

---

## Troubleshooting

| Problem | Fix |
|---|---|
| `accounts.shopify.com refused to connect` on install | Make sure **"Embedded in Shopify admin"** is OFF in the Partner app config. |
| OAuth callback "Invalid state or store" | Token expired (10 min). Click Install again. |
| Re-auth gives `No client_id/client_secret available` | Either set `SHOPIFY_CLIENT_ID/SECRET` in `.env`, or open the store's `⋯` menu and paste a per-store client_id/secret. |
| Backups silently skip customers | Set `BACKUP_ENCRYPTION_KEY=$(openssl rand -hex 32)` in `.env` and restart. Customer PII is never written unencrypted. |
| `Token rejected by Shopify` on paste-token | The token doesn't have `read_products` (we hit `shop.json` to verify). Re-issue it with full scopes. |
| 401 from the API | JWT expired (7d). Sign in again. |

---

## Security notes

- Passwords are stored as bcrypt hashes (cost 10).
- JWTs are signed with `APP_SECRET` and expire in 7 days.
- Shopify access tokens are stored in plaintext in SQLite (encrypted at rest is your responsibility — use disk encryption / SQLCipher if needed).
- Customer PII is AES-256-GCM encrypted with `BACKUP_ENCRYPTION_KEY` before being written to disk.
- The OAuth callback verifies state + shop domain. HMAC is verified when possible.
- All API endpoints require a valid JWT. Store endpoints additionally verify the user has access.

---

## Endpoints reference

```
POST /api/users/signup            email, password
POST /api/users/login             email, password  (or just password for v1 legacy)
GET  /api/users/me                requires JWT
POST /api/users/password          current_password, new_password
POST /api/users/logout

GET  /api/users                   admin: list all
PATCH /api/users/:id              admin: change role
DELETE /api/users/:id             admin: remove user
GET  /api/users/:id/stores        admin: list user's stores
POST /api/users/:id/stores        admin: grant store access
DELETE /api/users/:id/stores/:storeId   admin: revoke

GET  /api/oauth/config            App URL / Redirect URL / Scopes
POST /api/oauth/install           {shop_domain} → {authorize_url}
POST /api/oauth/reauth/:storeId   → {authorize_url}
GET  /api/auth/callback           Shopify OAuth callback (set this in Partner Dashboard)

GET  /api/admin/stores            list (scoped to user)
POST /api/admin/stores            manual create
DELETE /api/admin/stores/:id      delete
POST /api/admin/stores/:id/token  paste shpat_ token
POST /api/admin/stores/:id/backup trigger backup

GET  /api/admin/backups[?store_id=]
GET  /api/admin/backups/:id
GET  /api/admin/backups/:id/tree
GET  /api/admin/backups/:id/file?path=
GET  /api/admin/backups/:id/download  (zip stream)

POST /api/admin/restore           {backup_id, collector, selector, target_store_id?}
POST /api/admin/restore/:id/confirm
GET  /api/admin/restores

GET  /api/admin/settings          admin
POST /api/admin/settings          admin
GET  /api/admin/logs
```
