Note
This project is under active development.
Real Estate company demo website and testing ground for numerous backends.
So far, I am working with Drizzle and SQLite, although I expect to use Prisma with MongoDB, and Golang too.
This present document serves as a General Design Document.
The project is structured as a monorepo managed by pnpm workspaces.
A general overview of its current state:
graph TD
subgraph FE ["Frontend"]
UI[UI Components]
Components("`Geographic Maps
Property Listing
**User Interactions**`")
end
subgraph BE ["Backend"]
API[Hono REST API]
ORM[Drizzle ORM]
end
subgraph SH ["Shared Package"]
Schema[Database Schema]
Types[Shared Data Types]
end
DB[(SQLite Database)]
%% Runtime Data Flow
Components ==> UI
UI <== "Fetch / JSON" ==> API
API <== "Queries" ==> ORM
ORM <== "SQL" ==> DB
%% Source of Truth Logic
Schema -- "Defines" --> ORM
Schema -- "Infers" --> Types
Schema -- "Syncs / Migrates" --> DB
Types -- "Type Safety" --> UI
Types -- "Type Safety" --> API
%% Subgraphs
style FE fill:#1f1f1f,stroke:#202036,stroke-width:2px
style BE fill:#1f1f1f,stroke:#202036,stroke-width:2px
style SH fill:#1f1f1f,stroke:#202036,stroke-width:2px
%% Lighter Node
style UI fill:#3f3f3f,stroke:#909090,color:#f5f5f5
style Components fill:#3f3f3f,stroke:#909090,color:#f5f5f5
style API fill:#3f3f3f,stroke:#909090,color:#f5f5f5
style ORM fill:#3f3f3f,stroke:#909090,color:#f5f5f5
style Schema fill:#3f3f3f,stroke:#909090,color:#f5f5f5
style Types fill:#3f3f3f,stroke:#909090,color:#f5f5f5
style DB fill:#3f3f3f,stroke:#909090,color:#f5f5f5
The main parts consist of what follows (dependencies and their web links are listed in their respective README documents):
The frontend is designed to be a modern web application built with React Router v7.
It was decided to organize React components simply in /frontend/app/components/* and /frontend/app/components/*/*, where subdirectories respect to different routes.
With Tailwind CSS v4 We utilize a custom theme and general reset that extends preflight along with utility classes.
Global styles live in /frontend/app/css/, split by concern: tailwind-theme.css (theme tokens), tailwind-reset.css (the preflight extension), tailwind-general.css (layout primitives, typography and shared buttons), plus contact.css, forms.css and modals.css for their respective areas.
Component-layer classes must be declared inside @layer components. Leaving a rule unlayered makes it beat every layer, including utilities, which silently breaks the ability to override it inline from a className.
Slash opacities are restricted to ten rungs.
| Rung | Mostly used for | Typical use |
|---|---|---|
/12 |
Outlines | Hairline outlines on imagery; barely-there tints |
/28 |
Backgrounds | Skeleton fills; faint card tints; dashed empty-state borders |
/36 |
Backgrounds | Card and badge tints |
/40 |
Backgrounds; borders | Tinted surfaces and the borders that sit on them |
/48 |
Backgrounds; borders | Button borders; image scrims; decorative underlines |
/60 |
Backgrounds; borders | Panel backgrounds; dividers |
/64 |
Mixed | Panel tints; micro-labels; outlines |
/74 |
Text | Secondary prose and muted small print |
/84 |
Text | Body text on tinted cards, lead paragraphs |
/94 |
Backgrounds | Near-solid action buttons |
The general idea is that the low rungs carry backgrounds and borders,
/74and/84carry text, and/94is reserved for backgrounds that are almost solid./64is the one rung that genuinely does both.
opacity-less is 0.84 and opacity-lesser is 0.6, sitting on the same ladder rungs. Bare opacity-* utilities do it as well.
React-leaflet is used for interactive property maps that support adding markers. These are basically open street maps with some extra functionality.
React Router's loader and action patterns are used for data fetching and mutations, minimizing the need for global state libraries.
This extends to transient UI state that would otherwise live in useState. We use URL parameters for filtering the real estate properties and chat conversations, with the one currently open in the chat panel being held in the URL as a ?chat=<id> search parameter, which lets the route loader fetch the thread on the server, survives a page reload, and makes a given conversation linkable.
Chats are kept fresh by re-running the active loaders on an interval (see usePollingRevalidation) rather than by a socket.
RESTful API built with Hono, using Drizzle ORM.
It runs as a Cloudflare Worker; the schema and query layer use Cloudflare D1's SQLite-compatible driver.
- Endpoints:
- Properties:
GET /api/properties: Supports filtering (price, type, location, etcetera).GET /api/properties/:id: View of a single listing.GET /api/cities: Gets unique cities for search suggestions on user inputs.
- Authentication:
POST /api/auth/register: User registration.POST /api/auth/login: User login (sets session cookie).POST /api/auth/logout: User logout (clears session cookie).GET /api/auth/me: Verifies currently authenticated user session and retrieves the full user's profile.
- Users & Bookmarks:
GET /api/users: Gets list of user agents (not normal users, as this is for Our Agents page).GET /api/users/:id: Gets profile details of a specific user.PUT /api/users/:id: Updates a user's profile (name, profile picture, biography, etc).PUT /api/users/:id/password: Updates a user's scrypt password hash.POST /api/users/:id/promote: Promotes a normal user to Agent status using a secret code.GET /api/users/:id/properties: Gets properties owned by a user.GET /api/users/:id/bookmarks: Gets a user's bookmarked properties.POST /api/users/:id/bookmarks: Saves a property to a user's bookmarks.DELETE /api/users/:id/bookmarks/:propertyId: Removes a property from a user's bookmarks.
- Chats & Messages:
GET /api/chats: Gets the authenticated user's conversations, most recently active first, each with its counterpart, property, last message and unread count.GET /api/chats/unread-count: Counts the distinct people with unread messages, which is what the notification badge displays.GET /api/chats/:id/messages: Gets a single conversation with its full message history.POST /api/chats: Opens the conversation with an agent about one of their properties, reusing an existing one if it exists.POST /api/chats/:id/messages: Posts a message into a conversation.POST /api/chats/:id/read: Marks a conversation as read up to the present moment.
- Properties:
- Users: Represents real estate agents and their clients. It stores identity and profile information.
- Properties: The central entity. It stores listing details, location (latitude/longitude), pricing and media (images, galleries).
- Posts: Blog or news entries for the platform.
- Chats, Chat Participants & Messages: Persistent conversations between a user and an agent. Every chat is scoped to one property, so a conversation always has a subject; participants are a many-to-many join table that also carries each side's
lastReadAtread watermark, from which unread counts are derived without storing per-message read flags. - Bookmarks: Users are able to save favorite properties to look up later from their respective profile pages.
We use Drizzle.
JSON fields are used to store complex data like image galleries and nearby places within the SQLite database without requiring a whole new set of tables. There is a neat helper for this thanks to Drizzle's custom types.
A central package containing the database schema and TypeScript types, ensuring consistency across both aforementioned sub-repositories as the single source of truth; these are shared as a local dependency under the name of @free-real-estate/shared.
Begin by using the environment template:
cp ./backends/node-drizzle/.env.example ./backends/node-drizzle/.envThen run:
pnpm install
pnpm migrate:be:local
pnpm run dev⌃ This:
- Creates database tables according to the schema.
- Starts the Cloudflare backend Worker and frontend in parallel. The backend's daily demo reset is handled by its Cron Trigger; its data fixture remains linked from the shared general data file.
To load the demo data locally, invoke the scheduled handler after pnpm run dev is running:
curl http://localhost:8787/cdn-cgi/local/scheduledIn order of relevant importance:
- Session Refresh Concurrency: Refresh token rotation currently misbehaves when a single route loader fires several authenticated requests in parallel. The problem, the mitigation in place and the candidate fixes are documented in the refresh token rotation document.
- Backend Diversification: Implementing the same API specifications in Go to compare performance and developer experience could be very interesting from a certain perspective.
- Messaging System: What remains could be making delivery real-time instead of polled, plus niceties such as an emoji picker, attachments, ability to edit and delete messages, and typing indicators.
- Blogs: A blog feature with rich text support. I am delaying this for a good while.













