FindSpace
unknownA room rental marketplace where tenants search listings, message landlords directly, and landlords manage their own inventory.
Runs on Render's free tier; the first request after idle takes roughly 30 seconds.
Architecture
- Next.js 14 client (Vercel, Pages Router), client. Renders search, listing detail, messaging, and the landlord dashboard, and holds the auth token for every API call. Connected to Axios + JWT interceptor. Connected from GitHub Actions CI over build + deploy on push.
- Axios + JWT interceptor, client. Attaches the bearer token to every outgoing request and funnels 401 responses into a single logout path. Connected from Next.js 14 client. Connected to JWT OncePerRequestFilter over REST + JWT.
- JWT OncePerRequestFilter, service. Validates the bearer token and populates the Spring Security context before any controller method runs. Connected from Axios + JWT interceptor over REST + JWT. Connected to Service layer.
- Service layer, service. Holds the business rules: who may edit a listing, when a delete becomes a soft delete, what a conversation may contain. Connected from JWT OncePerRequestFilter. Connected to JPA repositories.
- JPA repositories, service. Maps the five domain entities to Postgres and generates the queries the service layer asks for. Connected from Service layer. Connected to HikariCP pool.
- HikariCP pool, service. Bounds concurrent database connections from the API. Connected from JPA repositories. Connected to PostgreSQL 15 over JDBC.
- PostgreSQL 15 (Neon), datastore. The single durable store: users, listings, conversations, messages, and the soft-delete flags. Connected from HikariCP pool over JDBC.
- GitHub Actions CI, external. Builds both apps on every push and pull request, so a broken build never reaches a deploy hook. Connected to Next.js 14 client over build + deploy on push.
- Boundary: Render container, Spring Boot 3.2 REST API, Docker.
Trace
liveFires one real read-only request at the deployed Spring Boot API on Render and times each hop.
Decisions
Delete is a runtime decision
- The decision
- Deleting a listing that already has conversations hides it and preserves the message history; a listing with no history is removed outright; editing a hidden listing reactivates it.
- The constraint that forced it
- Tenants keep message threads that reference listings, so a hard delete would orphan live conversations.
- Rejected
- Always soft-deleting. It leaves orphan rows that nothing will ever read.
- Consequence accepted
- Two delete paths to test instead of one, and every listing query has to respect the hidden flag.
Nullable column before backfill
- The decision
- A live schema migration landed the new column as a nullable wrapper type, so the column could deploy ahead of the data.
- The constraint that forced it
- Adding a NOT NULL column to an already-populated table crashed Hibernate on boot in production.
- Rejected
- Wiping and reseeding the table. Fine in development, data loss in production.
- Consequence accepted
- Application code treats the field as optional until a backfill and a follow-up migration tighten the constraint.
Configuration lives in the environment
- The decision
- Datasource, JWT secret, and CORS origin are read from environment variables, so dev and prod differ by environment and not by branch.
- The constraint that forced it
- Three hosts (Vercel, Render, Neon) need different origins and credentials for the same build artifact.
- Rejected
- Per-environment property files in the repository. They leak secrets and they drift.
- Consequence accepted
- A missing variable fails at boot rather than at build, so the deploy checklist has to be respected.
Known limitations
- The backend sleeps on the free tier, so the first request after idle takes roughly 30 seconds.
- No refresh token rotation. When a token expires, the session simply ends.
- Search is a SQL LIKE across title, address, and description, which will not survive a real listing volume. The next step is a Postgres full-text index.