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/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 20+)
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` and `customer.subscription.deleted`.
139
- Copy the signing secret into `STRIPE_WEBHOOK_SECRET`.
140
- (Local testing: `stripe listen --forward-to localhost:3000/webhooks/stripe`.)
141
- 4. **Class packs** sell immediately via Checkout (price taken from the product;
142
- optionally paste a Stripe Price ID on the product for Stripe-side pricing).
143
- 5. **Memberships** are Stripe subscriptions: create a recurring Price in Stripe,
144
- paste its `price_...` id into the plan in Admin → Products.
145
-
146
- Fulfillment is idempotent (keyed on the Checkout session id) — Stripe's webhook
147
- retries can't double-credit. Card data never touches your server; clients pay on
148
- Stripe-hosted Checkout. Set `BASE_URL` so success/cancel redirects and emailed
149
- links use your public domain.
150
-
151
- ## Email (optional)
152
-
153
- Set `SMTP_HOST` (+ `SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`/`SMTP_FROM`) to send
154
- booking confirmations, cancellation notices, waitlist promotions and magic
155
- links. Without SMTP, every email is written to `data/outbox/` as a `.eml` file
156
- and magic links are shown directly in the UI — everything remains testable.
157
-
158
- ## Security
159
-
160
- What's protected out of the box:
161
-
162
- - **CSRF**: every state-changing form carries a session-bound token; POSTs
163
- without a valid token get a 403. The Stripe webhook is exempt — it is
164
- authenticated by Stripe's signature over the raw body instead.
165
- - **Rate limiting** (in-memory fixed window, per IP + route): magic-link
166
- requests 5/15 min, admin login 10/15 min, public booking/buy POSTs
167
- 30/15 min. Over the limit → friendly 429. Behind a reverse proxy, set
168
- `TRUST_PROXY=1` so limits key on the first `X-Forwarded-For` hop; without
169
- it that header is ignored (it's spoofable).
170
- - Passwords are bcrypt-hashed; client self-service uses expiring HMAC-signed
171
- magic links (no client passwords); sessions are signed `SameSite=Lax`
172
- `HttpOnly` cookies; webhook fulfillment is idempotent.
173
-
174
- What's *not* there yet — plan accordingly:
175
-
176
- - **No 2FA** on staff logins.
177
- - The rate limiter is **single-instance and in-memory**: counters are
178
- per-process and reset on restart. Fine for the one-container target; a
179
- multi-instance deployment needs a shared store (or limit at the proxy).
180
- - **HTTPS is your reverse proxy's job** — run behind Caddy/Traefik/nginx.
181
-
182
- ## Backups
183
-
184
- Admin → Settings → **Download backup** produces a consistent snapshot via
185
- SQLite `VACUUM INTO`. Or just copy `data/studio.db` while the app is stopped.
186
- It's one file — cron it anywhere.
187
-
188
- ## Screenshots
189
-
190
- **Public booking site** — what your clients see. No account needed; the waiver is
191
- collected on first booking, and returning clients are matched by email so membership
192
- or pack credits apply automatically.
193
-
194
- | Schedule | Booking a class |
195
- |---|---|
196
- | ![Public schedule](docs/screenshots/public-schedule.png) | ![Booking form](docs/screenshots/public-booking.png) |
197
-
198
- **Buy page** — class packs and memberships, on *your* Stripe account (or pay-at-studio
199
- if you haven't connected Stripe).
200
-
201
- ![Buy page](docs/screenshots/public-buy.png)
202
-
203
- **Admin** — dashboard, schedule, class roster with one-tap check-in, client profiles,
204
- and revenue reports.
205
-
206
- | Dashboard | Roster & check-in |
207
- |---|---|
208
- | ![Admin dashboard](docs/screenshots/admin-dashboard.png) | ![Roster](docs/screenshots/admin-roster.png) |
209
-
210
- | Schedule management | Client profile | Reports |
211
- |---|---|---|
212
- | ![Schedule](docs/screenshots/admin-schedule.png) | ![Client](docs/screenshots/admin-client.png) | ![Reports](docs/screenshots/admin-reports.png) |
213
-
214
- ## Development
215
-
216
- ```sh
217
- npm install
218
- npm test # node:test + supertest, no network
219
- npm run dev # --watch mode
220
- ```
221
-
222
- Stack: Node 20+, Express, better-sqlite3 (WAL), EJS + HTMX + Pico.css
223
- (vendored). No frontend build. Tests cover the booking engine (capacity races,
224
- waitlist FIFO, credit deduct/refund, late-cancel policy), schedule
225
- materialization across timezones, the web flows, importer idempotency, and
226
- Stripe webhook fulfillment against a mock client.
227
-
228
- ## Honest v0.1 limitations
229
-
230
- - **Single studio, single location, one timezone.**
231
- - See [Security](#security) for what is and isn't covered (no 2FA;
232
- single-instance in-memory rate limiter; HTTPS via your reverse proxy).
233
- - Membership renewal bookkeeping is driven by Stripe webhooks; cash memberships
234
- need manual renewal (mark paid each cycle).
235
- - Monthly-credit memberships reset on a simple cycle from `cycle_started_on`;
236
- no proration.
237
- - Emails are plain text; no branded HTML templates yet.
238
- - English UI only (currency is a setting — no hardcoded symbols).
239
- - Reports are month-granularity tables + CSV; no charts.
240
-
241
- ## Roadmap
242
-
243
- - Multi-location support
244
- - Branded/installable PWA per studio (custom colors, icon, name)
245
- - SMS reminders (Twilio-compatible, bring-your-own account)
246
- - Recurring cash membership invoicing + renewal reminders
247
- - Retail/POS, gift cards, video — explicitly out of scope for now, as are staff
248
- payroll, per-instructor payouts, marketing automation, native apps, GDPR
249
- export tooling and i18n.
250
-
251
- ## License
252
-
253
- MIT
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
+ | ![Public schedule](docs/screenshots/public-schedule.png) | ![Booking form](docs/screenshots/public-booking.png) |
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
+ ![Buy page](docs/screenshots/public-buy.png)
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
+ | ![Admin dashboard](docs/screenshots/admin-dashboard.png) | ![Roster](docs/screenshots/admin-roster.png) |
226
+
227
+ | Schedule management | Client profile | Reports |
228
+ |---|---|---|
229
+ | ![Schedule](docs/screenshots/admin-schedule.png) | ![Client](docs/screenshots/admin-client.png) | ![Reports](docs/screenshots/admin-reports.png) |
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 20+, Express, better-sqlite3 (WAL mode), server-rendered EJS templates + HTMX
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. When unset, all
100
- purchase flows fall back to manual/"pay at studio" and the admin sees a "Stripe not connected"
101
- hint. When set (test mode is fine): packs via Checkout one-time, memberships via Checkout
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