Adds the plumbing that makes "can a customer actually get booked" true end to end at the API layer, on top of #15's schema/tenancy layer. - resource_hours table + min_notice_minutes/max_advance_days/buffer_minutes on resources -- config #15 didn't include but #16 depends on. - availability.py: pure slot-generation function, correct across a Europe/Berlin DST transition (tested both directions). - booking_api.py: JSON blueprint for slot listing, booking creation (auto_confirm -> confirmed/pending), and signed-JWT cancel/reschedule, registered into app.py. - booking_db.py gains resource-hours CRUD, a tenant-scoped busy-bookings query for buffer/slot validation, and a read-only client lookup. A true concurrent-threads test (not just sequential requests) surfaced a real gap: Postgres can raise DeadlockDetected instead of ExclusionViolation when two overlapping inserts race the exclusion constraint directly, which went uncaught and would have 500'd instead of giving the clean 4xx the ticket requires -- now caught alongside ExclusionViolation. Also fixed: reschedule used the request's raw UTC offset to pick the business day instead of the client's own timezone (could pick the wrong day's hours/bookings near local midnight); the cancel/reschedule JWT no longer falls back to reusing CRM_API_TOKEN as its signing secret. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
smb-online — "Get Your Small Business Online"
A productized service that gets local small businesses online: landing page + online booking + lead capture + small automations. Built to be thoroughly tracked and logged — every client, lead, booking and invoice lives in a Postgres CRM (source of truth), driven by n8n on the homelab, with a one-way mirror into Google Sheets for a human-readable overview / Looker Studio dashboard.
Full documentation (architecture, data model, every component):
Documentation.md.
Delivery tiers — an internal signal, not a fixed product
tier (A = free Google stack, B = self-hosted/privacy) started as two fixed product bundles but
is being decoupled from the public offering (see TODO.md): which stack a client actually gets
is a decision made with the client, case-by-case, and recorded on their clients row
(tier, stack_notes) — it's no longer baked into the demos. In practice, booking has already
converged on one shared self-hosted service (Easy!Appointments, see deploy/booking/) used for
every client regardless of tier.
Repository layout
Documentation.md Full architecture / component reference — start here for "how it works"
backoffice/ Postgres CRM + Flask API + operator dashboard (the source of truth)
deploy/
clients/ One isolated nginx stack per signed client + new-client.sh scaffolder
booking/ Shared Easy!Appointments booking service (all clients)
smb-demos/ Public demo/preview hosting (Apache)
backup/ Daily Postgres backup script (cron on the homelab)
n8n/ Exported n8n workflows (lead-intake, onboarding, booking-sync, renewal-reminder)
playbooks/ Operator playbooks (outreach, lead-to-customer, tier-a, tier-b)
sheets/SCHEMA.md Google Sheets mirror spec (tabs + columns) — downstream of Postgres
templates/landing/ Landing-page demos + client preview pages
demo-tier-a/ "Schnittpunkt" barbershop demo
demo-tier-b/ "PhysioVital" physio practice demo
demo-pizzeria/ "Bella Napoli" pizzeria demo w/ online ordering
templates/onboarding/ Internal "create client" form → n8n onboarding workflow
View the demos
They are self-contained HTML — open directly:
xdg-open templates/landing/demo-tier-a/index.html # barbershop
xdg-open templates/landing/demo-tier-b/index.html # physio practice
xdg-open templates/landing/demo-pizzeria/index.html # pizzeria / online ordering
Or serve them all with the chooser page:
python3 -m http.server 8080 --directory templates/landing
# → http://localhost:8080/ (links to all three demos)
Lead/callback forms POST live to the n8n lead-intake webhook; booking widgets link to the
shared Easy!Appointments instance. Nothing here is stubbed anymore — see Documentation.md §4.5
for the full workflow wiring.
Phases (status)
Tracked against actual repo state — see Documentation.md for what backs each checkmark.
- Phase 1 — Demo landing pages
- Phase 2 — Accounts & setup: Google account + workbook, n8n live, Google creds bound,
Vaultwarden (
clients.vault_ref), domain (wildcard*.mivanchenko.de) - Phase 3 — CRM (now Postgres-first, Sheets mirrored) + n8n lead-intake & renewal workflows — Looker Studio dashboard itself lives in Google and isn't repo-verifiable
- Phase 4 — Demo forms + booking wired live to n8n webhooks (lead-intake, booking-sync)
- Phase 5 — First real client + reception v1 — onboarding automation and per-client
deploy tooling exist and are exercised (
deploy/clients/); not verifiable from the repo alone whether a paying client is live - Phase 6 — AI receptionist (Claude Haiku 4.5) — not started
⚠️ Before going live (legal — not legal advice)
Operator is in an Ausbildung as Cloud Engineer at NETWAYS. This offering deliberately stays out
of NETWAYS's lane (no managed cloud hosting / monitoring / OpenStack/K8s hosting for clients).
Before taking paying clients: (1) check the Ausbildungsvertrag for a Nebentätigkeit clause and
get written employer approval if required; (2) Gewerbeanmeldung + Kleinunternehmerregelung
(§19 UStG) once revenue starts; (3) confirm with the IHK / a Steuerberater if unsure.