Architecture plan
निष्ठाThree decisions
Everything built so far — the schema, the reference-id link, the webhook handler and its earn/redeem/refund logic — runs entirely locally. None of it has anywhere to live yet, no one can log into it, and it doesn't know how to talk to the site people will actually land on. These three calls are what unblock deployment, in the order they were raised.
Render, on a paid always-on tier — not the free one.
Why not free-tier: a sleeping dyno delays webhook delivery, and a delayed stamp reads to a customer as a broken product, not a slow one. node:sqlite also needs a persistent disk, which rules out serverless hosts like Vercel or Netlify outright — their filesystems are wiped between invocations.
Leaning Postgres now rather than at the Phase 2 gate.
Why move it up: server/db.js is deliberately the only file that touches the database, and there's no real data yet to migrate — this is the cheapest this move will ever be. Doing it now also sidesteps the whole "does this host handle a persistent SQLite file correctly" question for good, rather than deferring a known Phase 2 task under pilot-launch time pressure.
Cafes sign in with Square OAuth — no separate password system. Customers get no login at all, by design.
Why one flow, not two: Square OAuth is already Phase 2 work (a cafe connects its own account instead of a pasted token) — using it as the dashboard login too closes the "no auth at all" gap and that OAuth item in a single build, instead of standing up a parallel email/password system a non-technical owner will eventually need reset. Customer auth would break the product's actual pitch: the card code is the only credential, by design.
Keep it single-origin — the same Express app already serves public/* and /api/*.
Why not split it out: a separate frontend host means CORS, a second deploy pipeline, and a second place webhooks or auth could quietly drift out of sync. If the three-column hero becomes the real marketing site, that's a file merge into public/ — not new infrastructure.
System shape
Square is the only external system of record for identity and payment. Everything niṣṭhā owns — the stamp ledger, the reward state, the webhook audit trail — lives in one place.
Browser
Customer / cafe
Signup, wallet card, dashboard — static pages, no build step
Till hardware
Square checkout
Staff scan the card's barcode inside Square's own POS
Render — always on
Express app (server/index.js)
public/* static · /api/* product routes · /api/webhooks/square — signature-verified, 200-then-process
This app owns it
Postgres
cafes · customers · transactions · webhook_events — the append-only ledger
Square owns it
Customers, Payments, Orders
Identity and money never live in niṣṭhā's database
Data model
The four original tables plus the two added for the webhook flow carry over to Postgres unchanged in shape — this is a storage-engine swap, not a redesign.
| Table | Carries |
|---|---|
| cafes | Loyalty rule, brand, square_loyalty_discount_id — the id that tells a real redemption apart from a comped drink |
| customers | card_code — the same string as Square's referenceId — plus live stamp/reward counts |
| transactions | Append-only ledger: earn / redeem / reverse, linked to the Square payment that caused it |
| webhook_events | Every inbound event, by id — dedupe, audit trail, and retry queue in one table |
Two more arrive with the auth decision above, not the database swap: a cafes.square_oauth_token (encrypted at rest) replacing the pasted access token, and a session store for the dashboard login itself.
Rollout
Gate: a stamp lands on a phone because a real Square transaction completed — not a button click.
Gate: you'd be comfortable if a stranger's phone number were in the database tonight.
Gate: a month of real use where regulars trust their stamp count.
webhook_events table — expired tokens, failed deliveries, missing punchesImmediate next steps
SQUARE_WEBHOOK_NOTIFICATION_URL at it.build