Start here
Architecture
One stateless API, one relational source of truth, a fast in-memory layer, and background workers for anything that can wait. Small enough for one person to run, built so each piece can scale on its own.
Swipe sideways to see the whole figure.
Components#
| Layer | Technology | What it is responsible for |
|---|---|---|
| Mobile app | Expo SDK 57, React Native, Expo Router, TanStack Query | Map search, booking, payments, QR pass, host dashboard, QR scanner |
| Landing site | Next.js 16 on Vercel | This website, the APK download, a health page that checks the API and database |
| API | Python 3.12, FastAPI, Pydantic v2, SQLAlchemy 2 (async) | 89 routes: validation, auth, business rules, pricing, the booking state machine |
| Database | PostgreSQL + PostGIS on Neon | Users, listings, slots, bookings, payments, ledger, outbox. Constraints enforce the invariants |
| Cache & queue | Redis (Redis Cloud) | Search cache, rate-limit counters, one-time QR/challenge tokens, bloom filter, Celery broker |
| Workers | Celery worker + beat | Deliver outbox events, expire unpaid holds, mark no-shows, reconcile payments, cleanup |
| Storage | Backblaze B2 (S3 API) | Listing and dispute photos, uploaded directly from the phone with presigned URLs |
| Providers | Razorpay, Resend, Expo Push, Clerk | Payments, transactional email, push notifications, Google sign-in |
Life of a request#
What happens between a tap in the app and the response, using POST /v1/bookings as the example. Each step can stop the request early with a clear JSON error, so expensive work only happens for requests that deserve it.
Request guard
middlewareRejects bodies over 1 MiB (413) before reading them; adds request id, timing and security headers.
Validation
PydanticTypes, ranges and formats checked: bad input is a 422 with field-level messages. NUL bytes anywhere are a 400.
Authentication
JWTShort-lived access token verified; token version compared so a password change signs out every device.
Rate limit
RedisSliding-window counter per user or IP. Over the limit: 429 with Retry-After.
Service + domain rules
one transactionPricing, state machine, availability. Domain events are written to the outbox in the same transaction.
Commit succeeds
201 with the booking. A worker is woken to deliver the outbox events.
Constraint violated
Overlap or duplicate key becomes a clean 409, never a 500.
Database unavailable
Pool wait capped at 3 s, then 503 with Retry-After instead of hanging.
Code layout#
The backend separates HTTP from business rules, so the same rules run in the API and in background workers.
parkezy-innotech/backend/apptext
api/ # HTTP only: routes, dependencies, error mapping, middleware v1/ # auth, me, public, bookings, host, reviews, support, admin, webhooks …core/ # config, security (argon2id, JWT), errors, logging, clockdomain/ # pure rules: pricing, booking_state, availability, geo, access codesservices/ # use cases: booking, payment, ledger, listing, auth, login protection …infra/ # Redis helpers: cache, rate limiter, bloom filter, one-time tokens, metricsintegrations/ # ports to the outside: payments, email, push, storage, identity, CAPTCHAworkers/ # Celery tasks: outbox processor, maintenance, reconciliationcontainer.py # composition root: builds every dependency once, tests swap piecesDeployment#
| Piece | Where it runs | Notes |
|---|---|---|
| API + worker | Render (Docker, Oregon) | One container runs uvicorn and the Celery worker, with migrations applied on start |
| Database | Neon Postgres (us-east-2) | Pooled connections via PgBouncer |
| Redis | Redis Cloud (us-east-2) | Next to the database, so a login's two Redis round trips stay short |
| Landing + docs | Vercel | Static pages plus one server route for the health check |
| Android app | EAS Build | Release APK; secrets live in EAS environment variables, never in the repo |
Configuration and secrets#
- Infrastructure settings come from environment variables, validated at startup. The app refuses to boot with, say, Resend selected but no key, or short JWT secrets in production.
- Business rules (GST, commission, hold times, overstay fees) live in a
platform_settingstable, so an admin can change them without a deploy. - No key of any kind is committed to the repository: deployment files mark secrets
sync: falseand they are set in each platform's dashboard.