Browser (phone / laptop) Host PC or small server
┌───────────────────────────┐ HTTP/JSON ┌───────────────────────────────┐
│ public/index.html │ ───────────▶ │ server.js (Node.js ≥ 22.13) │
│ public/app.js (vanilla JS)│ ◀─────────── │ └─ src/app.js Express routes│
│ public/styles.css │ session │ ├─ src/auth.js scrypt, │
│ Leaflet + OpenStreetMap │ cookie │ │ sessions, cookies │
└───────────────────────────┘ │ ├─ src/pricing.js quote() │
│ map tiles │ └─ src/db.js node:sqlite │
▼ │ │ │
tile.openstreetmap.org │ data/carshare.db (SQLite) │
└───────────────────────────────┘
node:sqlite is plenty and needs no native build or DB server.project3/
├─ server.js entry point (reads env vars, opens DB, starts HTTP)
├─ src/
│ ├─ app.js createApp(db, options) — all routes & validation
│ ├─ auth.js password hashing, sessions, cookie helpers, middleware
│ ├─ db.js schema + openDb() + transaction()
│ ├─ pricing.js quote(car, start, end) for database rows
│ └─ rules.js pure pricing/booking rules, shared with hosted/
├─ public/ frontend (index.html, app.js, styles.css)
│ └─ images/cars/ bundled demo photos (credits in docs/IMAGE_CREDITS.md)
├─ scripts/seed.js demo data
├─ scripts/build-hosted.js inlines src/rules.js into hosted/index.html
├─ tests/ node:test suites (API, rules, hosted parity)
├─ hosted/ claude.ai hosted version (single HTML page)
├─ data/ SQLite database and uploads/ (created at runtime)
└─ docs/ these documents
users 1───* cars 1───* availability
│ │
│ └──────* bookings *───1 users (renter)
└──1───* sessions
| Table | Key columns | Notes |
|---|---|---|
users |
id, username (unique, case-insensitive), display_name, password_hash, password_salt, created_at | scrypt 64-byte hash, 16-byte salt |
sessions |
token (PK, 256-bit random), user_id, expires_at | 14-day expiry; expired rows purged on login |
cars |
id, owner_id, make, model, year, seats, fuel, transmission, color, image_url, image_credit, description, price_per_hour, price_per_day, lat, lng, address, is_listed | is_listed=0 hides the car from others |
availability |
id, car_id, start_at, end_at | Owner-offered windows; merged when overlapping/touching |
bookings |
id, car_id, renter_id, start_at, end_at, total_price, status (confirmed/cancelled), note |
Price frozen at booking time |
Photos. image_url is one of: a bundled demo photo (/images/cars/<name>.jpg), an uploaded photo (/uploads/<random>.<ext>, stored in data/uploads/), or an http(s) link. image_credit holds the licence credit for bundled photos, and is cleared automatically when an owner changes the photo. openDb() adds the image_credit column to databases created before it existed.
All times are stored as UTC ISO-8601 strings (2030-01-01T09:00:00.000Z), so string comparison equals time comparison. The browser converts to and from local time.
A booking [start, end) for car C by user U is accepted only if:
end > start, and start isn’t more than 10 minutes in the past;[start, end);b.start < end AND b.end > start). Back-to-back bookings are allowed.All bodies are JSON. Errors return { "error": "message" } with a 4xx status. All routes except /api/auth/* need a session cookie (otherwise 401).
| Method & path | Who | Purpose |
|---|---|---|
POST /api/auth/register |
anyone | {username, password, displayName?, inviteCode?} → 201 + cookie. 403 if full or wrong invite code, 409 if username taken |
POST /api/auth/login |
anyone | {username, password} → cookie |
POST /api/auth/logout |
anyone | clears session |
GET /api/auth/me |
anyone | {user|null, members, maxUsers, inviteRequired, currency, defaultCenter} |
GET /api/cars?q&seats&fuel&maxPrice&from&to |
member | Listed cars + available_now, next_window; with from/to returns only free cars plus a quote |
GET /api/my/cars |
member | My cars including unlisted |
GET /api/cars/:id |
member | {car, availability[], bookings[]} (renter names only for owner/renter) |
GET /api/cars/:id/quote?start&end |
member | {quote:{hours,days,remHours,total}, unavailable: reason|null} |
POST /api/cars |
member | Create a car (fields as in cars table) |
PUT /api/cars/:id |
owner | Partial update — e.g. {price_per_hour, price_per_day, is_listed} |
DELETE /api/cars/:id |
owner | 409 if the car has upcoming bookings |
POST /api/cars/:id/availability |
owner | {start, end} — add (merged) window |
DELETE /api/cars/:id/availability/:wid |
owner | 409 if an upcoming booking sits inside it |
POST /api/uploads |
member | Raw image body (Content-Type: image/jpeg, image/png or image/webp, ≤ 5 MB) → 201 {url}. The type is checked from the file’s first bytes; 400 if it isn’t a real image |
GET /uploads/<file> |
anyone | Serves an uploaded photo |
POST /api/bookings |
member | {carId, start, end, note?} → 201, or 409 with a reason |
GET /api/bookings |
member | {asRenter[], asOwner[]} |
POST /api/bookings/:id/cancel |
renter or owner | Cancel a booking that hasn’t ended |
Single page with three tabs:
Secure when SECURE_COOKIES=1).INVITE_CODE.| Variable | Default | Meaning |
|—|—|—|
| PORT | 3080 | HTTP port |
| HOST | 0.0.0.0 | Bind address (0.0.0.0 = reachable on your network) |
| DB_FILE | data/carshare.db | SQLite file path |
| MAX_USERS | 10 | Member cap |
| INVITE_CODE | (none) | If set, needed to register |
| CURRENCY | $ | Currency symbol shown in the UI |
| DEFAULT_LAT / DEFAULT_LNG | 51.5074 / -0.1278 | Map centre when there are no cars, and centre for seed data |
| SECURE_COOKIES | (off) | Set to 1 when served over HTTPS |
hosted/index.html is the same product as a single page published on claude.ai. It uses the platform’s shared database and sign-in (claude.ai accounts) instead of the Express server. Photos live in the page’s own file store, because the hosted page can’t load images from other websites. Car documents keep imageId and imageUrl (/_blob/<id>). Uploading is available only to people who can edit the page. See the user guide for the differences.
Shared rules. Pricing, availability cover, booking overlap and window merging live in src/rules.js, which has no dependencies and works on either epoch ms or ISO strings. The server uses it directly. npm run build:hosted copies it verbatim into hosted/index.html between the // BEGIN src/rules.js and // END src/rules.js markers, and the page wraps it in small adapters for its own field names (pricePerHour, windows as { s, e }). CI runs npm run check:hosted and tests/hosted.test.js, so a stale copy or a broken adapter fails the build. After changing src/rules.js, run npm run build:hosted and republish the page.
Intentional differences: the hosted page drops availability windows that ended more than a day ago when saving, because they are stored inside the car document (the server keeps old rows and filters on read). Error messages are also worded differently.