feat: RBAC, atomic deployments, fleet backups, and licensing (Pro) (#185)

* feat: add RBAC viewer accounts, atomic deployments, and fleet-wide backups (Pro)

Introduces three Pro-tier features:

- RBAC: Multi-user system with admin/viewer roles, user management UI,
  automatic migration from single-admin credentials, viewer restrictions
  across the entire UI (read-only editor, hidden action buttons)

- Atomic Deployments: Pre-deploy file backup to .sencho-backup/, automatic
  rollback on health probe failure, manual rollback button, health probes
  added to stack updates, webhook-triggered deploys use atomic rollback

- Fleet-Wide Backups: Point-in-time snapshots of compose files across all
  nodes (local + remote), stored centrally in SQLite, per-stack restore
  with optional redeploy, graceful handling of offline nodes

* fix(settings): use correct ProGate prop name in UsersSection

* fix(settings): remove unused isPro prop from UsersSection

* fix(auth): fetch user info after login and setup so isAdmin is set correctly

* feat(pricing): revise pricing strategy and enforce variant-based seat limits

Raise Personal Pro from $49/yr to $69/yr with 3 viewer seats (up from 1).
Add $15/mo billing option for Team Pro. Mark lifetime pricing as a
90-day early-adopter offer. Store Lemon Squeezy variant_name on
activation/validation and enforce seat limits server-side per variant.

* feat(licensing): add Lemon Squeezy checkout, webhook, and billing portal integration

Server-side checkout URL generation (POST /api/checkout) with admin email
pre-fill and instance_id custom data. HMAC-SHA256 verified webhook endpoint
(POST /api/webhooks/lemonsqueezy) handling order, subscription, and payment
lifecycle events for automatic license activation. Customer billing portal
link stored from webhook events and exposed via GET /api/billing/portal.
In-app checkout buttons in Settings with manual license key fallback.

* fix(licensing): exempt Lemon Squeezy webhook from auth middleware

The catch-all auth middleware on /api/* was blocking the public webhook
endpoint. Added /webhooks/lemonsqueezy to the exemption list alongside
/auth/* and /webhooks/:id/trigger.

* feat(pricing): update pricing to final live rates

Personal Pro: $7.99/month, $69.99/year, $249 lifetime.
Team Pro: $49.99/month, $499.99/year, $1,499 lifetime.
Added personal_monthly checkout variant across backend, frontend, and website.

* refactor(licensing): remove server-side checkout/webhook for self-hosted model

Sencho is self-hosted — each user runs their own instance, so there is
no central server to receive webhooks or hold the store API key. Replaced
in-app checkout buttons with a "View Pricing" redirect to sencho.io and
kept manual license key activation as the primary flow.

- Delete LemonSqueezyService (checkout, webhook, HMAC verification)
- Remove POST /api/checkout, GET /api/billing/portal, POST /api/webhooks/lemonsqueezy
- Remove raw body parser and auth exemption for webhook route
- Remove all LEMONSQUEEZY_* env vars from .env.example
- Replace checkout buttons in SettingsModal with single "View Pricing" button
- Simplify LicenseContext checkout to open sencho.io pricing page
- Update licensing docs to reflect website-based purchase flow

* chore: normalize em-dashes to hyphens across codebase (linter)

* chore: remove accidentally tracked directories from index
This commit is contained in:
Anso
2026-03-26 21:58:24 -04:00
committed by GitHub
parent 269ea6fe53
commit 32a7d53b2b
54 changed files with 721 additions and 543 deletions
+3 -3
View File
@@ -14,13 +14,13 @@ Sencho stores all its state in two places: the **data directory** (SQLite databa
Default path: `/app/data` inside the container, mapped to wherever you mounted it on the host (e.g. `./sencho-data`).
Contains:
- `sencho.db` SQLite database with all settings, nodes, alerts, metrics history, and notification history
- `sencho.db` - SQLite database with all settings, nodes, alerts, metrics history, and notification history
This single file is everything Sencho knows about itself. Back it up and you can fully restore any Sencho installation.
### 2. Compose directory (`COMPOSE_DIR`)
The directory containing your stack subdirectories your `compose.yaml` files, `.env` files, and any bind-mounted config files stored there.
The directory containing your stack subdirectories - your `compose.yaml` files, `.env` files, and any bind-mounted config files stored there.
This is your actual application data. It lives entirely outside Sencho and you almost certainly already have it on a schedule, but include it in any Sencho backup plan.
@@ -131,7 +131,7 @@ If other Sencho instances were pointing to your old host as a remote node, updat
| Item | Location | Notes |
|------|----------|-------|
| Container data volumes | Wherever each stack's volumes are mounted on the host | Back these up separately per-application |
| Actual container images | Docker image cache | These are re-pulled on next deploy no backup needed |
| Actual container images | Docker image cache | These are re-pulled on next deploy - no backup needed |
| Sencho logs (docker logs) | Container stdout | Not persisted beyond container lifetime |
<Note>
+9 -9
View File
@@ -15,13 +15,13 @@ docker compose -f /path/to/your/stack/compose.yaml logs
The most common causes:
- **Missing environment variable** a required variable in your `.env` file is empty or has the wrong name.
- **Port already in use** another container or host process is bound to the same port. Change the host port in the compose file.
- **Volume path does not exist** a bind-mount path on the host doesn't exist yet. Create the directory manually.
- **Missing environment variable** - a required variable in your `.env` file is empty or has the wrong name.
- **Port already in use** - another container or host process is bound to the same port. Change the host port in the compose file.
- **Volume path does not exist** - a bind-mount path on the host doesn't exist yet. Create the directory manually.
---
## The 1:1 path rule volumes resolve to wrong paths
## The 1:1 path rule - volumes resolve to wrong paths
**Symptom:** Stacks deploy but relative volume paths (e.g. `./config:/config`) point to the wrong location inside the container, or `docker compose` exits with a path error.
@@ -37,7 +37,7 @@ environment:
- COMPOSE_DIR=/opt/docker
```
See [Configuration the 1:1 path rule](/getting-started/configuration#compose-directory-the-11-path-rule) for a full explanation.
See [Configuration - the 1:1 path rule](/getting-started/configuration#compose-directory-the-11-path-rule) for a full explanation.
---
@@ -61,7 +61,7 @@ ls -la /var/run/docker.sock
# Expected: srw-rw---- 1 root docker ...
```
If the group is not `docker`, the auto-detection still works Sencho reads the GID from the socket file at startup.
If the group is not `docker`, the auto-detection still works - Sencho reads the GID from the socket file at startup.
---
@@ -95,7 +95,7 @@ proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
```
Traefik handles WebSocket upgrades automatically for HTTP/1.1 backends no extra config needed.
Traefik handles WebSocket upgrades automatically for HTTP/1.1 backends - no extra config needed.
---
@@ -106,7 +106,7 @@ Traefik handles WebSocket upgrades automatically for HTTP/1.1 backends — no ex
**Checks in order:**
1. **Is the remote Sencho instance running?** SSH to that machine and verify.
2. **Is the API URL correct?** It must include the protocol and port (e.g. `http://192.168.1.20:3001`). Open it in a browser you should see a JSON response from `/api/health`.
2. **Is the API URL correct?** It must include the protocol and port (e.g. `http://192.168.1.20:3001`). Open it in a browser - you should see a JSON response from `/api/health`.
3. **Is the token correct?** Tokens are long JWT strings. Even one missing character will cause auth to fail. Regenerate the token on the remote instance and update the node config.
4. **Is there a firewall blocking the port?** The primary Sencho host must be able to reach the remote host's Sencho port.
@@ -131,7 +131,7 @@ sqlite3 /path/to/data/sencho.db
DELETE FROM global_settings WHERE key IN ('auth_username', 'auth_password_hash', 'auth_jwt_secret');
```
4. Restart the container the setup screen will appear on next visit.
4. Restart the container - the setup screen will appear on next visit.
<Warning>
This resets authentication entirely. All active sessions become invalid. Your stacks, nodes, and alert rules are not affected.