Deployment¶
What this answers: where does each component from Components physically run, and what's the operational shape?
This page is a stub until #19 resolves the M1 production hosting decision. The diagram and notes below capture the leading candidate.
Diagram (planned)¶
graph TB
subgraph Internet
User[Visitor / Builder]
end
subgraph Hosting["Hosting (TBD per #19)"]
direction TB
WebApp[Next.js web app<br/>Node 22]
Workers[BullMQ scraper workers<br/>Node 22]
end
subgraph DBTier["Database tier"]
direction TB
PG[(Postgres 17<br/>Neon serverless<br/>or Hetzner VPS)]
RD[(Redis<br/>Upstash or self-hosted)]
end
subgraph SaaS["External SaaS"]
direction TB
Resend[Resend<br/>email]
Auth[Auth.js<br/>Google + magic link]
end
subgraph CFP["Cloudflare Pages"]
Docs[961tech docs<br/>this site]
end
User -->|HTTPS| WebApp
User -.->|HTTPS| Docs
WebApp -->|Prisma over TLS| PG
WebApp -->|BullMQ over TLS| RD
WebApp -->|HTTPS| Resend
WebApp -->|OAuth/SMTP| Auth
Workers -->|Prisma over TLS| PG
Workers -->|BullMQ| RD
Workers -->|HTTPS scrape| Internet
Hosting candidates (#19)¶
| Option | Pros | Cons |
|---|---|---|
| bits.lb beta | Lebanese hosting — politically aligned with the product (a Lebanese tool), beta pricing, edge-close to users | Beta — uptime SLO unknown, may need fallback |
| Hetzner | Cheap, reliable, EU-located but performant from LB | DIY ops — backups, monitoring, OS patching |
| Vercel + Neon + Upstash | Zero ops, gen-1-ready | More expensive at scale; Vercel cold starts may matter |
| Railway / Fly.io | Middle ground — managed but cheap | Less generous free tiers than Vercel |
The decision will be captured as an ADR when made.
Operational characteristics¶
Web app¶
- Sizing: small at M1 (single Lebanese-market user base, low concurrent traffic). One container is enough.
- Stateless: sessions are JWT or cookie-signed; no in-memory state. Trivial to add a second instance behind a load balancer when needed.
- Cold-start sensitivity: matters on Vercel-style serverless; not on a long-running container.
Scraper workers¶
- Sizing: scales with retailers × categories × refresh frequency. M1 = 3 × 8 × daily ≈ 24 jobs/day. Trivial.
- Concurrency: small — one job at a time per retailer is polite. Total active jobs likely < 10.
- Resilience: failed jobs retry with exponential backoff. Persistent failures alert via #19 observability hooks (TBD).
Postgres¶
- Sizing: small — low-millions of rows max for years. Smallest tier on any provider works.
- Backups: managed providers handle this. If self-hosted, daily logical backups + WAL shipping; restored-from-backup runbook needed before going live.
- Connection pooling: Prisma's built-in pool, plus PgBouncer if hosting via Hetzner.
Redis¶
- Sizing: tiny — only queue state, no application caching. 100MB is plenty.
- Resilience: queue state is recoverable from DB if Redis is wiped (jobs would re-enqueue). Don't over-invest in HA Redis.
Docs (this site)¶
- Cloudflare Pages — already live at https://961tech.pages.dev. See Runbooks → Deploy docs site. No backend, fully static; uptime is whatever CF gives.
AI discoverability surface¶
Per ADR-0013 the M1 surface is served by the Next.js app itself, not an edge worker:
/robots.txt— generated bysrc/app/robots.ts(open posture: every named training, AI-search, on-demand, and conventional UA getsAllow: /; wildcard rule disallows/api/go/). Sitemap directive points at${NEXT_PUBLIC_BASE_URL}/sitemap.xml./sitemap.xml— generated bysrc/app/sitemap.ts. Covers homepage,/build, every category page (/products?category=…), every/build/choose/[category], and everyProductrow (withlastModifiedfromProduct.updatedAt)./llms.txt— static file atpublic/llms.txt, ≤5KB curated index per ADR-0013 D2.- JSON-LD —
src/lib/structured-data.tscomposers (Product,AggregateOffer,Offer,Brand,BreadcrumbList,Organization) rendered via theStructuredDataserver component. Emitted on product detail pages only whenmatchStatus ∈ {auto, manual}(ADR-0013 D3) and on the homepage asOrganization. - OG / Twitter Card — sitewide defaults in
src/app/layout.tsx; per-product overrides viagenerateMetadatainsrc/app/products/[slug]/page.tsx.
Production deploy needs NEXT_PUBLIC_BASE_URL set to the canonical origin (defaults to https://961tech.pages.dev) so Sitemap: and JSON-LD url fields resolve correctly.
Networking¶
- Public surfaces: the web app (HTTPS), the docs site (HTTPS).
- Private: Postgres, Redis. Reachable only from web app + workers via VPC or IP allowlist.
- Outbound from workers: unrestricted HTTPS for scraping. Workers may need a static egress IP if a retailer ever rate-limits or geofences.
Backups & disaster recovery¶
Pending decision. RTO/RPO targets TBD as part of #19. At M1 scale, "manual restore from yesterday's backup" is acceptable.
Observability¶
Pending. Likely:
- Logs — structured JSON, shipped to a log sink (provider-managed initially)
- Metrics — basic CF Analytics on the docs; web app metrics minimum: response time p95, error rate
- Alerts — Telegram (matches MASTER's existing setup); see brain repo for the channel.
- Scraper drift alerts — when a parser returns 0 listings unexpectedly, that's a parse break
For local dev, see Guides → Local setup.