studio-os 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -3
- package/.gitattributes +8 -0
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +49 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +30 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- package/.github/dependabot.yml +33 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/guards.yml +45 -0
- package/CHANGELOG.md +159 -86
- package/README.md +270 -253
- package/SECURITY.md +27 -0
- package/SPEC.md +4 -4
- package/package.json +54 -54
- package/runner-lock.json +15 -0
- package/src/app.js +163 -141
- package/src/db/003-stripe-invoices.sql +6 -0
- package/src/db/index.js +83 -82
- package/src/routes/me.js +1 -1
- package/src/routes/public.js +13 -3
- package/src/services/auth.js +88 -81
- package/src/services/booking.js +19 -13
- package/src/services/stripe.js +44 -1
- package/test/booking-link.test.js +47 -0
- package/test/deps.test.js +108 -0
- package/test/error-paths.test.js +49 -0
- package/test/membership-cycle.test.js +78 -0
- package/test/proxy-auth.test.js +114 -0
- package/test/stripe-halfconfig.test.js +98 -0
- package/test/stripe.test.js +84 -1
- package/test/web.test.js +10 -1
- package/views/admin/dashboard.ejs +1 -1
- package/views/admin/settings.ejs +7 -2
- package/views/public/book_result.ejs +5 -5
- package/PROGRESS.md +0 -60
package/README.md
CHANGED
|
@@ -1,253 +1,270 @@
|
|
|
1
|
-
# Studio OS
|
|
2
|
-
|
|
3
|
-
Self-hosted booking, class-pack and membership management for boutique studios —
|
|
4
|
-
yoga, pilates, martial arts, climbing, dance, small gyms. The Mindbody escape hatch.
|
|
5
|
-
|
|
6
|
-
**The structural difference no SaaS rival can copy:** payments run on *your own*
|
|
7
|
-
Stripe account. Studio OS never touches the money — no processing markup, no
|
|
8
|
-
per-booking fees, no contracts. Run it in one Docker container or with plain
|
|
9
|
-
Node on a $5 VPS or a spare machine at the studio.
|
|
10
|
-
|
|
11
|
-
- Public schedule + booking with waiver capture — clients need no passwords
|
|
12
|
-
(signed magic links for self-service cancellation)
|
|
13
|
-
- Class packs (credits), memberships (unlimited or N/month), drop-ins
|
|
14
|
-
- Atomic capacity, FIFO waitlist with auto-promotion, cancellation-window policy
|
|
15
|
-
- Admin: rosters + check-in, clients, manual sales, revenue/attendance reports + CSV,
|
|
16
|
-
one-click SQLite backup
|
|
17
|
-
- Instructor logins: per-class assignments, a scoped schedule + roster portal
|
|
18
|
-
with attendee check-in, zero admin access
|
|
19
|
-
- Mindbody CSV importer (clients + pricing options), idempotent and dry-runnable
|
|
20
|
-
- Stripe Checkout for packs and membership subscriptions (optional), SMTP email
|
|
21
|
-
(optional — without it, emails land in `data/outbox/` and links show on screen)
|
|
22
|
-
- Everything server-rendered (EJS + HTMX + Pico.css, all vendored — no CDN, no
|
|
23
|
-
build step, works offline), SQLite in one file, PWA manifest + service worker
|
|
24
|
-
|
|
25
|
-
## 5-minute quickstart
|
|
26
|
-
|
|
27
|
-
### Docker
|
|
28
|
-
|
|
29
|
-
```sh
|
|
30
|
-
git clone <this repo> studio-os && cd studio-os
|
|
31
|
-
docker compose up -d
|
|
32
|
-
# open http://localhost:3000 → setup wizard creates your studio + owner login
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
The database persists in the `studio-data` volume. Configure SMTP/Stripe by
|
|
36
|
-
uncommenting the `environment:` block in `docker-compose.yml`.
|
|
37
|
-
|
|
38
|
-
Verified end-to-end (Docker Engine 28.x, linux/amd64, `node:22-slim` base —
|
|
39
|
-
better-sqlite3 uses its prebuilt glibc binary, no compiler stage needed):
|
|
40
|
-
|
|
41
|
-
```sh
|
|
42
|
-
docker compose build
|
|
43
|
-
docker compose up -d
|
|
44
|
-
curl -i http://localhost:3000/ # 302 → /setup on a fresh volume
|
|
45
|
-
# setup wizard → admin one-off class → public schedule → guest booking: all OK
|
|
46
|
-
docker compose down -v # stop + remove the data volume
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Bare Node (Node
|
|
50
|
-
|
|
51
|
-
```sh
|
|
52
|
-
git clone <this repo> studio-os && cd studio-os
|
|
53
|
-
npm install
|
|
54
|
-
npm start
|
|
55
|
-
# open http://localhost:3000 → setup wizard
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
The database is a single file at `data/studio.db` (override with `DB_PATH`).
|
|
59
|
-
Copy `.env.example` to `.env` to configure port, SMTP, Stripe. Nothing is
|
|
60
|
-
required — with zero config the app runs fully offline.
|
|
61
|
-
|
|
62
|
-
Want to look around with data first?
|
|
63
|
-
|
|
64
|
-
```sh
|
|
65
|
-
npm run seed # demo studio: classes, weekly schedule, clients, passes
|
|
66
|
-
npm start # admin login: owner@example.com / studio-demo
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### First-run walkthrough
|
|
70
|
-
|
|
71
|
-
1. Setup wizard: studio name, timezone, currency, owner account.
|
|
72
|
-
2. Admin → Class types: create your classes (duration, capacity, drop-in price, credits).
|
|
73
|
-
3. Admin → Schedule: add weekly rules — instances materialize on a rolling
|
|
74
|
-
8-week horizon automatically (at boot, daily, and on rule changes).
|
|
75
|
-
4. Share the public schedule (`/`). Clients book with just name + email and sign
|
|
76
|
-
the waiver on first booking.
|
|
77
|
-
5. Sell packs/memberships from a client's profile (manual), or connect Stripe
|
|
78
|
-
for online purchase.
|
|
79
|
-
6. Optional: Admin → Instructors → "Add instructor login", then tick the
|
|
80
|
-
classes they teach. Instructors sign in on the same staff login page and get
|
|
81
|
-
a portal with just their schedule and rosters (with check-in) — no admin
|
|
82
|
-
access.
|
|
83
|
-
|
|
84
|
-
## Migrating from Mindbody
|
|
85
|
-
|
|
86
|
-
You need two CSV exports from Mindbody:
|
|
87
|
-
|
|
88
|
-
1. **Clients** — Reports → Clients (or Clients → Export). Standard columns:
|
|
89
|
-
`First Name, Last Name, Email, Mobile Phone, Notes, Liability Waiver, ...`
|
|
90
|
-
2. **Pricing options** (remaining passes) — Reports → Pricing Options /
|
|
91
|
-
"Remaining sessions". Standard columns:
|
|
92
|
-
`Client, Email, Pricing Option, Remaining, Total, Expiration, Price Paid, ...`
|
|
93
|
-
|
|
94
|
-
Then either paste the CSVs into **Admin → Import** (dry-run checkbox included),
|
|
95
|
-
or run the CLI:
|
|
96
|
-
|
|
97
|
-
```sh
|
|
98
|
-
# preview first — prints every row's action, writes nothing
|
|
99
|
-
node scripts/import-mindbody.mjs --clients clients.csv --passes pricing.csv --dry-run
|
|
100
|
-
|
|
101
|
-
# real import
|
|
102
|
-
node scripts/import-mindbody.mjs --clients clients.csv --passes pricing.csv
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Behavior:
|
|
106
|
-
|
|
107
|
-
- **Idempotent by email** — re-running updates existing clients/passes, never
|
|
108
|
-
duplicates. Safe to re-export from Mindbody and re-import on cutover day.
|
|
109
|
-
- Rows without a valid email are skipped and reported (Mindbody allows
|
|
110
|
-
email-less clients; Studio OS keys everything on email).
|
|
111
|
-
- Passes for emails not in the clients file auto-create a bare client record.
|
|
112
|
-
- A signed "Liability Waiver = Yes" column marks the waiver as signed so
|
|
113
|
-
clients aren't re-prompted.
|
|
114
|
-
- Non-standard column names? Pass `--mapping mapping.json`:
|
|
115
|
-
|
|
116
|
-
```json
|
|
117
|
-
{
|
|
118
|
-
"clients": { "email": ["E-Mail-Adresse"], "firstName": ["Vorname"] },
|
|
119
|
-
"passes": { "remaining": ["Sessions Left"] }
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Keys you provide override the defaults; everything else keeps sane defaults
|
|
124
|
-
(see `DEFAULT_MAPPING` in `src/services/importer.js`). Dates accept `M/D/YYYY`
|
|
125
|
-
and ISO; money accepts `$1,500.00`-style strings.
|
|
126
|
-
|
|
127
|
-
## Stripe setup (optional, bring-your-own account)
|
|
128
|
-
|
|
129
|
-
Without Stripe, every purchase flow falls back to "pay at studio / FPS" and you
|
|
130
|
-
activate purchases manually from the client profile — fully usable cash-only.
|
|
131
|
-
|
|
132
|
-
1. Create a [Stripe](https://stripe.com) account (test mode is fine to start).
|
|
133
|
-
2. Set env vars (`.env` or compose environment):
|
|
134
|
-
- `STRIPE_SECRET_KEY` — Developers → API keys
|
|
135
|
-
- `STRIPE_PUBLISHABLE_KEY`
|
|
136
|
-
- `STRIPE_WEBHOOK_SECRET` — see step 3
|
|
137
|
-
3. Add a webhook endpoint in Stripe: `https://your-domain/webhooks/stripe`,
|
|
138
|
-
events `checkout.session.completed
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
Stripe
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
links
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
- **
|
|
177
|
-
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
**
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
1
|
+
# Studio OS
|
|
2
|
+
|
|
3
|
+
Self-hosted booking, class-pack and membership management for boutique studios —
|
|
4
|
+
yoga, pilates, martial arts, climbing, dance, small gyms. The Mindbody escape hatch.
|
|
5
|
+
|
|
6
|
+
**The structural difference no SaaS rival can copy:** payments run on *your own*
|
|
7
|
+
Stripe account. Studio OS never touches the money — no processing markup, no
|
|
8
|
+
per-booking fees, no contracts. Run it in one Docker container or with plain
|
|
9
|
+
Node on a $5 VPS or a spare machine at the studio.
|
|
10
|
+
|
|
11
|
+
- Public schedule + booking with waiver capture — clients need no passwords
|
|
12
|
+
(signed magic links for self-service cancellation)
|
|
13
|
+
- Class packs (credits), memberships (unlimited or N/month), drop-ins
|
|
14
|
+
- Atomic capacity, FIFO waitlist with auto-promotion, cancellation-window policy
|
|
15
|
+
- Admin: rosters + check-in, clients, manual sales, revenue/attendance reports + CSV,
|
|
16
|
+
one-click SQLite backup
|
|
17
|
+
- Instructor logins: per-class assignments, a scoped schedule + roster portal
|
|
18
|
+
with attendee check-in, zero admin access
|
|
19
|
+
- Mindbody CSV importer (clients + pricing options), idempotent and dry-runnable
|
|
20
|
+
- Stripe Checkout for packs and membership subscriptions (optional), SMTP email
|
|
21
|
+
(optional — without it, emails land in `data/outbox/` and links show on screen)
|
|
22
|
+
- Everything server-rendered (EJS + HTMX + Pico.css, all vendored — no CDN, no
|
|
23
|
+
build step, works offline), SQLite in one file, PWA manifest + service worker
|
|
24
|
+
|
|
25
|
+
## 5-minute quickstart
|
|
26
|
+
|
|
27
|
+
### Docker
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
git clone <this repo> studio-os && cd studio-os
|
|
31
|
+
docker compose up -d
|
|
32
|
+
# open http://localhost:3000 → setup wizard creates your studio + owner login
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The database persists in the `studio-data` volume. Configure SMTP/Stripe by
|
|
36
|
+
uncommenting the `environment:` block in `docker-compose.yml`.
|
|
37
|
+
|
|
38
|
+
Verified end-to-end (Docker Engine 28.x, linux/amd64, `node:22-slim` base —
|
|
39
|
+
better-sqlite3 uses its prebuilt glibc binary, no compiler stage needed):
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
docker compose build
|
|
43
|
+
docker compose up -d
|
|
44
|
+
curl -i http://localhost:3000/ # 302 → /setup on a fresh volume
|
|
45
|
+
# setup wizard → admin one-off class → public schedule → guest booking: all OK
|
|
46
|
+
docker compose down -v # stop + remove the data volume
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Bare Node (Node 22+)
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
git clone <this repo> studio-os && cd studio-os
|
|
53
|
+
npm install
|
|
54
|
+
npm start
|
|
55
|
+
# open http://localhost:3000 → setup wizard
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The database is a single file at `data/studio.db` (override with `DB_PATH`).
|
|
59
|
+
Copy `.env.example` to `.env` to configure port, SMTP, Stripe. Nothing is
|
|
60
|
+
required — with zero config the app runs fully offline.
|
|
61
|
+
|
|
62
|
+
Want to look around with data first?
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npm run seed # demo studio: classes, weekly schedule, clients, passes
|
|
66
|
+
npm start # admin login: owner@example.com / studio-demo
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### First-run walkthrough
|
|
70
|
+
|
|
71
|
+
1. Setup wizard: studio name, timezone, currency, owner account.
|
|
72
|
+
2. Admin → Class types: create your classes (duration, capacity, drop-in price, credits).
|
|
73
|
+
3. Admin → Schedule: add weekly rules — instances materialize on a rolling
|
|
74
|
+
8-week horizon automatically (at boot, daily, and on rule changes).
|
|
75
|
+
4. Share the public schedule (`/`). Clients book with just name + email and sign
|
|
76
|
+
the waiver on first booking.
|
|
77
|
+
5. Sell packs/memberships from a client's profile (manual), or connect Stripe
|
|
78
|
+
for online purchase.
|
|
79
|
+
6. Optional: Admin → Instructors → "Add instructor login", then tick the
|
|
80
|
+
classes they teach. Instructors sign in on the same staff login page and get
|
|
81
|
+
a portal with just their schedule and rosters (with check-in) — no admin
|
|
82
|
+
access.
|
|
83
|
+
|
|
84
|
+
## Migrating from Mindbody
|
|
85
|
+
|
|
86
|
+
You need two CSV exports from Mindbody:
|
|
87
|
+
|
|
88
|
+
1. **Clients** — Reports → Clients (or Clients → Export). Standard columns:
|
|
89
|
+
`First Name, Last Name, Email, Mobile Phone, Notes, Liability Waiver, ...`
|
|
90
|
+
2. **Pricing options** (remaining passes) — Reports → Pricing Options /
|
|
91
|
+
"Remaining sessions". Standard columns:
|
|
92
|
+
`Client, Email, Pricing Option, Remaining, Total, Expiration, Price Paid, ...`
|
|
93
|
+
|
|
94
|
+
Then either paste the CSVs into **Admin → Import** (dry-run checkbox included),
|
|
95
|
+
or run the CLI:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
# preview first — prints every row's action, writes nothing
|
|
99
|
+
node scripts/import-mindbody.mjs --clients clients.csv --passes pricing.csv --dry-run
|
|
100
|
+
|
|
101
|
+
# real import
|
|
102
|
+
node scripts/import-mindbody.mjs --clients clients.csv --passes pricing.csv
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Behavior:
|
|
106
|
+
|
|
107
|
+
- **Idempotent by email** — re-running updates existing clients/passes, never
|
|
108
|
+
duplicates. Safe to re-export from Mindbody and re-import on cutover day.
|
|
109
|
+
- Rows without a valid email are skipped and reported (Mindbody allows
|
|
110
|
+
email-less clients; Studio OS keys everything on email).
|
|
111
|
+
- Passes for emails not in the clients file auto-create a bare client record.
|
|
112
|
+
- A signed "Liability Waiver = Yes" column marks the waiver as signed so
|
|
113
|
+
clients aren't re-prompted.
|
|
114
|
+
- Non-standard column names? Pass `--mapping mapping.json`:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"clients": { "email": ["E-Mail-Adresse"], "firstName": ["Vorname"] },
|
|
119
|
+
"passes": { "remaining": ["Sessions Left"] }
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Keys you provide override the defaults; everything else keeps sane defaults
|
|
124
|
+
(see `DEFAULT_MAPPING` in `src/services/importer.js`). Dates accept `M/D/YYYY`
|
|
125
|
+
and ISO; money accepts `$1,500.00`-style strings.
|
|
126
|
+
|
|
127
|
+
## Stripe setup (optional, bring-your-own account)
|
|
128
|
+
|
|
129
|
+
Without Stripe, every purchase flow falls back to "pay at studio / FPS" and you
|
|
130
|
+
activate purchases manually from the client profile — fully usable cash-only.
|
|
131
|
+
|
|
132
|
+
1. Create a [Stripe](https://stripe.com) account (test mode is fine to start).
|
|
133
|
+
2. Set env vars (`.env` or compose environment):
|
|
134
|
+
- `STRIPE_SECRET_KEY` — Developers → API keys
|
|
135
|
+
- `STRIPE_PUBLISHABLE_KEY`
|
|
136
|
+
- `STRIPE_WEBHOOK_SECRET` — see step 3
|
|
137
|
+
3. Add a webhook endpoint in Stripe: `https://your-domain/webhooks/stripe`,
|
|
138
|
+
events `checkout.session.completed`, `checkout.session.async_payment_succeeded`,
|
|
139
|
+
`invoice.paid` and `customer.subscription.deleted`. The async event is what
|
|
140
|
+
fulfils payments that clear later, like bank debits; card-only studios never
|
|
141
|
+
see it. `invoice.paid` records each membership renewal in the revenue report.
|
|
142
|
+
Copy the signing secret into `STRIPE_WEBHOOK_SECRET`.
|
|
143
|
+
(Local testing: `stripe listen --forward-to localhost:3000/webhooks/stripe`.)
|
|
144
|
+
The webhook secret is not optional: online payment stays off until it is
|
|
145
|
+
set, because the webhook is what turns a payment into a pass. Admin →
|
|
146
|
+
Settings tells you which half is missing.
|
|
147
|
+
4. **Class packs** sell immediately via Checkout (price taken from the product;
|
|
148
|
+
optionally paste a Stripe Price ID on the product for Stripe-side pricing).
|
|
149
|
+
5. **Memberships** are Stripe subscriptions: create a recurring Price in Stripe,
|
|
150
|
+
paste its `price_...` id into the plan in Admin → Products.
|
|
151
|
+
|
|
152
|
+
Fulfillment is idempotent (keyed on the Checkout session id) — Stripe's webhook
|
|
153
|
+
retries can't double-credit. Card data never touches your server; clients pay on
|
|
154
|
+
Stripe-hosted Checkout. Set `BASE_URL` so success/cancel redirects and emailed
|
|
155
|
+
links use your public domain.
|
|
156
|
+
|
|
157
|
+
## Email (optional)
|
|
158
|
+
|
|
159
|
+
Set `SMTP_HOST` (+ `SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`/`SMTP_FROM`) to send
|
|
160
|
+
booking confirmations, cancellation notices, waitlist promotions and magic
|
|
161
|
+
links. Without SMTP, every email is written to `data/outbox/` as a `.eml` file
|
|
162
|
+
and magic links are shown directly in the UI — everything remains testable.
|
|
163
|
+
|
|
164
|
+
## Security
|
|
165
|
+
|
|
166
|
+
What's protected out of the box:
|
|
167
|
+
|
|
168
|
+
- **CSRF**: every state-changing form carries a session-bound token; POSTs
|
|
169
|
+
without a valid token get a 403. The Stripe webhook is exempt — it is
|
|
170
|
+
authenticated by Stripe's signature over the raw body instead.
|
|
171
|
+
- **Rate limiting** (in-memory fixed window, per IP + route): magic-link
|
|
172
|
+
requests 5/15 min, admin login 10/15 min, public booking/buy POSTs
|
|
173
|
+
30/15 min. Over the limit → friendly 429. Behind a reverse proxy, set
|
|
174
|
+
`TRUST_PROXY=1` so limits key on the first `X-Forwarded-For` hop; without
|
|
175
|
+
it that header is ignored (it's spoofable).
|
|
176
|
+
- **Reverse-proxy awareness**: `TRUST_PROXY=1` also makes the app honour
|
|
177
|
+
`X-Forwarded-Proto`. TLS usually terminates at the proxy, so without it
|
|
178
|
+
every emailed magic link is built as `http://` — putting a 7-day auth token
|
|
179
|
+
on the wire in plaintext — and the session cookie never gets its `Secure`
|
|
180
|
+
flag. Set it, or pin `BASE_URL` to your https origin.
|
|
181
|
+
- Passwords are bcrypt-hashed; client self-service uses expiring HMAC-signed
|
|
182
|
+
magic links (no client passwords); sessions are signed `SameSite=Lax`
|
|
183
|
+
`HttpOnly` cookies (`Secure` too, over https); webhook fulfillment is
|
|
184
|
+
idempotent. The staff login does the same bcrypt work whether or not the
|
|
185
|
+
email exists, so it can't be used to enumerate accounts.
|
|
186
|
+
|
|
187
|
+
What's *not* there yet — plan accordingly:
|
|
188
|
+
|
|
189
|
+
- **No 2FA** on staff logins.
|
|
190
|
+
- **Without SMTP, client links are shown on screen**, so anyone who knows a
|
|
191
|
+
client's email can open their bookings page. That's fine for trying the app
|
|
192
|
+
out; configure SMTP before real clients use it. With SMTP on, links only
|
|
193
|
+
ever go to the inbox.
|
|
194
|
+
- The rate limiter is **single-instance and in-memory**: counters are
|
|
195
|
+
per-process and reset on restart. Fine for the one-container target; a
|
|
196
|
+
multi-instance deployment needs a shared store (or limit at the proxy).
|
|
197
|
+
- **HTTPS is your reverse proxy's job** — run behind Caddy/Traefik/nginx.
|
|
198
|
+
|
|
199
|
+
## Backups
|
|
200
|
+
|
|
201
|
+
Admin → Settings → **Download backup** produces a consistent snapshot via
|
|
202
|
+
SQLite `VACUUM INTO`. Or just copy `data/studio.db` while the app is stopped.
|
|
203
|
+
It's one file — cron it anywhere.
|
|
204
|
+
|
|
205
|
+
## Screenshots
|
|
206
|
+
|
|
207
|
+
**Public booking site** — what your clients see. No account needed; the waiver is
|
|
208
|
+
collected on first booking, and returning clients are matched by email so membership
|
|
209
|
+
or pack credits apply automatically.
|
|
210
|
+
|
|
211
|
+
| Schedule | Booking a class |
|
|
212
|
+
|---|---|
|
|
213
|
+
|  |  |
|
|
214
|
+
|
|
215
|
+
**Buy page** — class packs and memberships, on *your* Stripe account (or pay-at-studio
|
|
216
|
+
if you haven't connected Stripe).
|
|
217
|
+
|
|
218
|
+

|
|
219
|
+
|
|
220
|
+
**Admin** — dashboard, schedule, class roster with one-tap check-in, client profiles,
|
|
221
|
+
and revenue reports.
|
|
222
|
+
|
|
223
|
+
| Dashboard | Roster & check-in |
|
|
224
|
+
|---|---|
|
|
225
|
+
|  |  |
|
|
226
|
+
|
|
227
|
+
| Schedule management | Client profile | Reports |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
|  |  |  |
|
|
230
|
+
|
|
231
|
+
## Development
|
|
232
|
+
|
|
233
|
+
```sh
|
|
234
|
+
npm install
|
|
235
|
+
npm test # node:test + supertest, no network
|
|
236
|
+
npm run dev # --watch mode
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Stack: Node 22+, Express, better-sqlite3 (WAL), EJS + HTMX + Pico.css
|
|
240
|
+
(vendored). No frontend build. Tests cover the booking engine (capacity races,
|
|
241
|
+
waitlist FIFO, credit deduct/refund, late-cancel policy), schedule
|
|
242
|
+
materialization across timezones, the web flows, importer idempotency, and
|
|
243
|
+
Stripe webhook fulfillment against a mock client.
|
|
244
|
+
|
|
245
|
+
## Honest v0.1 limitations
|
|
246
|
+
|
|
247
|
+
- **Single studio, single location, one timezone.**
|
|
248
|
+
- See [Security](#security) for what is and isn't covered (no 2FA;
|
|
249
|
+
single-instance in-memory rate limiter; HTTPS via your reverse proxy).
|
|
250
|
+
- Membership renewal bookkeeping is driven by Stripe webhooks; cash memberships
|
|
251
|
+
need manual renewal (mark paid each cycle).
|
|
252
|
+
- Monthly-credit memberships reset on a simple cycle from `cycle_started_on`;
|
|
253
|
+
no proration.
|
|
254
|
+
- Emails are plain text; no branded HTML templates yet.
|
|
255
|
+
- English UI only (currency is a setting — no hardcoded symbols).
|
|
256
|
+
- Reports are month-granularity tables + CSV; no charts.
|
|
257
|
+
|
|
258
|
+
## Roadmap
|
|
259
|
+
|
|
260
|
+
- Multi-location support
|
|
261
|
+
- Branded/installable PWA per studio (custom colors, icon, name)
|
|
262
|
+
- SMS reminders (Twilio-compatible, bring-your-own account)
|
|
263
|
+
- Recurring cash membership invoicing + renewal reminders
|
|
264
|
+
- Retail/POS, gift cards, video — explicitly out of scope for now, as are staff
|
|
265
|
+
payroll, per-instructor payouts, marketing automation, native apps, GDPR
|
|
266
|
+
export tooling and i18n.
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
The latest tagged release is the only one that gets fixes. Self-hosted deployments have to update themselves.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Please **don't** open a public issue for a security problem.
|
|
10
|
+
|
|
11
|
+
Use GitHub's [private vulnerability reporting](https://github.com/Booyaka101/studio-os/security/advisories/new) instead. Expect a first response within a week.
|
|
12
|
+
|
|
13
|
+
Please include what you found, how to reproduce it, and what an attacker gets out of it.
|
|
14
|
+
|
|
15
|
+
## What this touches
|
|
16
|
+
|
|
17
|
+
Self-hosted. It holds your customers' booking data and talks to your own payment account. Nothing is sent to us.
|
|
18
|
+
|
|
19
|
+
- **It holds your customers' data**: names, contact details, bookings and pack balances, in your own database. Nothing is sent to us, ever.
|
|
20
|
+
- **Payments run through Stripe Checkout on your own account.** Card details are entered on Stripe's hosted page and never reach this application. Keep your webhook signing secret secret; an attacker holding it can forge payment events.
|
|
21
|
+
- **Sessions, CSRF and rate limiting** are in scope, and so is anything that lets one studio read another's data.
|
|
22
|
+
|
|
23
|
+
## Scope
|
|
24
|
+
|
|
25
|
+
In scope: anything that leaks a credential, reads data belonging to someone else, or lets untrusted input reach code execution.
|
|
26
|
+
|
|
27
|
+
Out of scope: findings that require an attacker to already control the machine it runs on.
|
package/SPEC.md
CHANGED
|
@@ -11,7 +11,7 @@ Deployment target: one Docker container (or `npm start`) on a $5 VPS or a spare
|
|
|
11
11
|
## v0.1 scope (MUST all work)
|
|
12
12
|
|
|
13
13
|
### Stack
|
|
14
|
-
- Node
|
|
14
|
+
- Node 22+, Express, better-sqlite3 (WAL mode), server-rendered EJS templates + HTMX
|
|
15
15
|
(vendored locally, NO CDN at runtime), vendored Pico.css + one small custom stylesheet.
|
|
16
16
|
No frontend build step. No paid APIs. Everything works offline except Stripe/SMTP.
|
|
17
17
|
- `node:test` + supertest for tests. Dockerfile + docker-compose.yml. `.env` config
|
|
@@ -96,9 +96,9 @@ files and magic links are surfaced in the UI (so everything is testable without
|
|
|
96
96
|
Templates: booking confirmation, cancellation, waitlist promotion, magic link.
|
|
97
97
|
|
|
98
98
|
### Stripe (optional, BYO account)
|
|
99
|
-
Env: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PUBLISHABLE_KEY.
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
Env: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PUBLISHABLE_KEY. Online payment needs
|
|
100
|
+
both the secret key and the webhook secret; with either missing, all purchase flows fall back
|
|
101
|
+
to manual/"pay at studio" and the admin sees which half is unset. When set (test mode is fine): packs via Checkout one-time, memberships via Checkout
|
|
102
102
|
subscription using `membership_plans.stripe_price_id` (admin field to paste a Price ID),
|
|
103
103
|
webhook fulfillment, and payment rows with the Stripe session id. NEVER store card data.
|
|
104
104
|
Write the integration against the official `stripe` npm package; all Stripe calls isolated in
|