A casual leasing platform for shopping centres. Enables tenants to browse, book, and manage temporary retail spaces (sites/kiosks) within shopping centres.
# Install dependencies
pnpm install
# Run development server
pnpm dev
# Build for production
pnpm build
# Start production server
pnpm start
# Type check
pnpm run check
# Run tests
pnpm test┌─────────────────────────────────────────────────────────────────┐
│ Client (React) │
│ - Vite + React 19 + TypeScript │
│ - TailwindCSS + Radix UI components │
│ - tRPC client for type-safe API calls │
│ - Google Maps (overview map, autocomplete) + MapLibre GL │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Server (Express + tRPC) │
│ - Express.js with tRPC router │
│ - Drizzle ORM for database operations │
│ - Dual auth: JWT password-based (primary) + OAuth fallback │
│ - Forge API proxy for storage, AWS SDK for other services │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ AWS Services │
│ - Storage: Forge API proxy (S3 SDK installed but secondary) │
│ - Location Service: Geocoding, places, routing │
│ - Bedrock: LLM (Claude), Image generation (Stable Diffusion) │
│ - Transcribe: Voice-to-text │
│ - EKS: Container orchestration │
│ - RDS PostgreSQL: Database │
└─────────────────────────────────────────────────────────────────┘
.
├── client/ # React frontend
│ └── src/
│ ├── components/ # Reusable UI components
│ ├── pages/ # Route pages
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Utilities (trpc client, etc.)
│ └── contexts/ # React contexts
│
├── server/ # Express backend
│ ├── _core/ # Core infrastructure
│ │ ├── index.ts # Server entry point
│ │ ├── trpc.ts # tRPC setup + procedure hierarchy
│ │ ├── context.ts # Dual auth (JWT + OAuth fallback)
│ │ ├── env.ts # Environment variables
│ │ ├── rateLimit.ts # IP-based login rate limiter
│ │ ├── authService.ts # Authentication logic
│ │ ├── amazonLocation.ts # AWS Location (geocoding, directions)
│ │ ├── llm.ts # AWS Bedrock LLM
│ │ ├── imageGeneration.ts # AWS Bedrock images
│ │ └── voiceTranscription.ts # AWS Transcribe
│ ├── routers.ts # Thin aggregator (~55 lines)
│ ├── routers/ # 17 domain-specific router files
│ │ ├── auth.ts, profile.ts, centres.ts, sites.ts
│ │ ├── bookings.ts, search.ts, admin.ts, users.ts
│ │ ├── usageCategories.ts, searchAnalytics.ts
│ │ ├── systemConfig.ts, faqs.ts, dashboard.ts
│ │ ├── budgets.ts, owners.ts, adminBooking.ts
│ │ └── assets.ts
│ ├── db.ts # Database queries
│ └── storage.ts # Forge API proxy for file storage
│
├── shared/ # Shared code (client + server)
│ └── types.ts # Shared TypeScript types
│
├── drizzle/ # Database schema & migrations
│ └── schema.ts # Drizzle ORM schema
│
├── k8s/ # Kubernetes manifests
│ ├── deployment.yml
│ ├── service.yml
│ ├── configmap.yml
│ └── ingress.yml
│
├── scripts/ # Utility scripts
│ ├── seed-admin-user.ts # Create admin user
│ ├── import-*.ts # Data import scripts
│ └── debug/ # 68 debug/test scripts (moved from root)
│
└── .github/workflows/ # CI/CD
└── deploy.yml # EKS deployment
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
JWT_SECRET |
Secret for JWT token signing |
AWS_REGION |
AWS region (default: ap-southeast-2) |
AWS_S3_BUCKET |
S3 bucket for file storage |
AWS_ACCESS_KEY_ID |
AWS credentials (or use IRSA) |
AWS_SECRET_ACCESS_KEY |
AWS credentials (or use IRSA) |
BUILT_IN_FORGE_API_URL |
Storage proxy base URL (required for file storage) |
BUILT_IN_FORGE_API_KEY |
Storage proxy API key (required for file storage) |
| Variable | Description | Default |
|---|---|---|
OAUTH_SERVER_URL |
OAuth server for legacy auth | - |
OWNER_OPEN_ID |
Owner OAuth identifier | - |
AMAZON_LOCATION_PLACE_INDEX |
Place index name | casuallease-place-index |
AMAZON_LOCATION_ROUTE_CALCULATOR |
Route calculator name | casuallease-route-calculator |
SMTP_HOST |
Email server host | - |
SMTP_PORT |
Email server port | 587 |
SMTP_USER |
Email username | - |
SMTP_PASS |
Email password | - |
SMTP_FROM |
Default from address | - |
File: server/storage.ts
Storage uses a Forge API proxy (BUILT_IN_FORGE_API_URL + BUILT_IN_FORGE_API_KEY), not direct AWS S3 SDK calls. The AWS S3 SDK is installed but only used for specific secondary operations.
import { storagePut, storageGet } from './storage';
// Upload a file
const { url } = await storagePut('path/to/file.jpg', buffer, 'image/jpeg');
// Get download URL
const { url } = await storageGet('path/to/file.jpg');File: server/_core/amazonLocation.ts
import { geocode, reverseGeocode, placesAutocomplete, getDirections } from './_core/amazonLocation';
// Address to coordinates
const result = await geocode('123 Main St, Sydney NSW');
// Coordinates to address
const result = await reverseGeocode(-33.8688, 151.2093);
// Place autocomplete suggestions
const suggestions = await placesAutocomplete('Westfield', { maxResults: 5 });
// Get directions between points
const route = await getDirections(
{ lat: -33.8688, lng: 151.2093 },
{ lat: -33.9173, lng: 151.2313 }
);Required AWS Resources:
- Place Index:
casuallease-place-index - Route Calculator:
casuallease-route-calculator
File: server/_core/llm.ts
import { invokeLLM } from './_core/llm';
const response = await invokeLLM({
model: 'anthropic.claude-3-haiku-20240307-v1:0',
messages: [
{ role: 'user', content: 'Describe this shopping centre...' }
],
maxTokens: 1000,
});File: server/_core/imageGeneration.ts
import { generateImage } from './_core/imageGeneration';
const { url } = await generateImage({
prompt: 'A modern shopping centre kiosk with LED lighting',
width: 1024,
height: 1024,
});File: server/_core/voiceTranscription.ts
import { transcribeAudio } from './_core/voiceTranscription';
const { text, segments } = await transcribeAudio(audioUrl, {
language: 'en-AU',
});Key tables in drizzle/schema.ts:
| Table | Description |
|---|---|
users |
User accounts (customers, admins, owners) |
customer_profiles |
Extended customer info (company, ABN, insurance) |
owners |
Shopping centre owners with bank details |
shopping_centres |
Shopping centre locations |
floor_levels |
Multi-level centre floors |
sites |
Bookable spaces within centres |
bookings |
Site reservations |
usage_categories |
Product/service categories |
transactions |
Payment records |
customer- Can browse and book sitesowner_centre_manager- Manage specific centreowner_state_admin- Manage centres in a statemega_state_admin- State-level admin accessmega_admin- Full system access
publicProcedure— anyone (no auth required)protectedProcedure— any logged-in userownerProcedure— any non-customer role (all owner_* + mega_state_admin + mega_admin)adminProcedure— mega_admin or mega_state_admin only
Dual auth system in server/_core/context.ts: password-based JWT (primary, 7-day sessions) with SDK/OAuth fallback. Login is rate-limited (5 attempts per 15min via server/_core/rateLimit.ts). A future Cognito integration is planned to replace the custom JWT auth.
- Add the procedure to the appropriate domain router in
server/routers/:
// server/routers/myDomain.ts
import { protectedProcedure } from "../_core/trpc";
import { z } from "zod";
import * as db from "../db";
export const myNewEndpoint = protectedProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input, ctx }) => {
// Access user via ctx.user
return await db.getMyData(input.id);
});If creating a new router file, register it in server/routers.ts.
- Add database function if needed in
server/db.ts:
export async function getMyData(id: number) {
return await db.select().from(myTable).where(eq(myTable.id, id));
}- Use in client:
const { data } = trpc.myRouter.myNewEndpoint.useQuery({ id: 1 });- Add to
drizzle/schema.ts:
export const myNewTable = pgTable("my_new_table", {
id: serial("id").primaryKey(),
name: varchar("name", { length: 255 }).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});- Generate and run migration:
pnpm run db:push- Create component in
client/src/pages/MyPage.tsx - Add route in
client/src/App.tsx:
<Route path="/my-page" component={MyPage} />Push to main triggers:
- Type check
- Docker build (ARM64)
- Push to ECR
- Deploy to EKS
- Run migrations
# Build Docker image
docker build -t casuallease .
# Push to ECR
aws ecr get-login-password | docker login --username AWS --password-stdin <account>.dkr.ecr.<region>.amazonaws.com
docker push <account>.dkr.ecr.<region>.amazonaws.com/casuallease:latest
# Apply to EKS
kubectl apply -f k8s/# Run migrations on pod
kubectl exec -n casuallease <pod> -- npx drizzle-kit push
# Seed admin user
kubectl exec -n casuallease <pod> -- npx tsx scripts/seed-admin-user.tsAfter seeding:
- Username:
admin - Password: Set via
scripts/seed-admin-user.ts
# Run all tests
pnpm test
# Run specific test file
pnpm test server/booking.test.ts
# Run with coverage
pnpm test -- --coverage# Copy data to pod
kubectl cp import-data.json casuallease/<pod>:/tmp/
# Run import
kubectl exec -n casuallease <pod> -- node scripts/import-centres.jskubectl logs -n casuallease -l app=casuallease -f# Connect via pod
kubectl exec -it -n casuallease <pod> -- node -e "
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// Run queries...
"| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite, TailwindCSS |
| UI Components | Radix UI, Lucide Icons |
| State | TanStack Query, tRPC |
| Backend | Express, tRPC, Node.js |
| Database | PostgreSQL, Drizzle ORM |
| Auth | JWT (7-day sessions), bcrypt, OAuth fallback |
| Maps | Google Maps (client), Amazon Location (server), MapLibre GL |
| Storage | Forge API proxy (S3 SDK secondary) |
| AI | AWS Bedrock (Claude, Stable Diffusion) |
| Deployment | Docker, Kubernetes (EKS) |
| CI/CD | GitHub Actions |
MIT