Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 5 additions & 13 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ CHAT_MODEL=gemini-3.1-flash-lite
GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id
GOOGLE_OAUTH_SECRET=your_google_oauth_secret

# Better Auth resolves OAuth callbacks against the backend origin, not the frontend.
# Generate the secret with `openssl rand -base64 32`.
BETTER_AUTH_SECRET=your_better_auth_secret
BETTER_AUTH_URL=http://localhost:3001

# Neon Postgres
# DATABASE_URL is the pooled endpoint (hostname ends in -pooler) used by the running app.
# DIRECT_URL is the unpooled endpoint used by Prisma Migrate, which cannot run through a
Expand All @@ -40,19 +45,6 @@ R2_ACCESS_KEY_ID=your_r2_access_key_id
R2_SECRET_ACCESS_KEY=your_r2_secret_access_key
R2_BUCKET=your_r2_bucket_name

# Supabase (auth only — Postgres and Storage have migrated to Neon and R2)
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
VITE_SUPABASE_URL=${SUPABASE_URL}
VITE_SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}

# Supabase Auth
# SITE_URL is where Supabase redirects users after email confirmation, password reset, etc.
# Set to your custom domain in production (e.g. https://yourapp.com), or your deployed
# app origin if you don't have a custom domain. Local dev uses http://localhost:5173.
SITE_URL=https://your-production-url.com

# Production canonical apex domain (hostname only, no protocol or path).
# When set in production, the backend 301-redirects the apex domain over HTTP
# and the www subdomain to https://<APEX_DOMAIN>.
Expand Down
89 changes: 43 additions & 46 deletions README.md

Large diffs are not rendered by default.

361 changes: 152 additions & 209 deletions RUNBOOK.md

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"@google/genai": "1.51.0",
"@prisma/client": "^6.6.0",
"@shared/types": "workspace:*",
"@supabase/supabase-js": "^2.49.4",
"better-auth": "^1.6.25",
"compression": "^1.8.1",
"cors": "^2.8.5",
Expand Down
2 changes: 0 additions & 2 deletions backend/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ const envSchema = z
DATABASE_URL: z.string().min(1),
DIRECT_URL: z.string().min(1),
GOOGLE_API_KEY: z.string().min(1),
SUPABASE_URL: z.string().url(),
SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
R2_ACCOUNT_ID: z.string().min(1),
R2_ACCESS_KEY_ID: z.string().min(1),
R2_SECRET_ACCESS_KEY: z.string().min(1),
Expand Down
5 changes: 0 additions & 5 deletions backend/src/config/supabase.ts

This file was deleted.

3 changes: 0 additions & 3 deletions backend/src/services/chat/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ const envMock = {
PORT: undefined,
BACKEND_PORT: 3001,
NODE_ENV: 'test' as const,
SUPABASE_DATABASE_URL: 'postgres://test',
GOOGLE_API_KEY: 'gmaps-key',
SUPABASE_URL: 'https://example.supabase.co',
SUPABASE_SERVICE_ROLE_KEY: 'srv',
FRONTEND_URL: 'http://localhost:5173',
PDF_TOKEN_SECRET: 'test-secret-at-least-32-characters-long-ok',
GEMINI_API_KEY: 'fallback-api-key' as string | undefined,
Expand Down
3 changes: 0 additions & 3 deletions backend/src/services/chat/__tests__/digest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
const TEST_ENV = {
BACKEND_PORT: 3001,
NODE_ENV: 'test',
SUPABASE_DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/solarsim',
GOOGLE_API_KEY: 'google-api-key',
SUPABASE_URL: 'https://example.supabase.co',
SUPABASE_SERVICE_ROLE_KEY: 'service-role-key',
FRONTEND_URL: 'http://localhost:5173',
PDF_TOKEN_SECRET: 'test-secret-at-least-32-characters-long-ok',
GEMINI_API_KEY: 'gemini-api-key',
Expand Down
3 changes: 0 additions & 3 deletions backend/src/services/chat/__tests__/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
const TEST_ENV = {
BACKEND_PORT: 3001,
NODE_ENV: 'test',
SUPABASE_DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/solarsim',
GOOGLE_API_KEY: 'google-api-key',
SUPABASE_URL: 'https://example.supabase.co',
SUPABASE_SERVICE_ROLE_KEY: 'service-role-key',
FRONTEND_URL: 'http://localhost:5173',
PDF_TOKEN_SECRET: 'test-secret-at-least-32-characters-long-ok',
GEMINI_API_KEY: 'gemini-api-key',
Expand Down
3 changes: 0 additions & 3 deletions backend/src/services/chat/__tests__/guardrails.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
const TEST_ENV = {
BACKEND_PORT: 3001,
NODE_ENV: 'test',
SUPABASE_DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/solarsim',
GOOGLE_API_KEY: 'google-api-key',
SUPABASE_URL: 'https://example.supabase.co',
SUPABASE_SERVICE_ROLE_KEY: 'service-role-key',
FRONTEND_URL: 'http://localhost:5173',
PDF_TOKEN_SECRET: 'test-secret-at-least-32-characters-long-ok',
GEMINI_API_KEY: 'gemini-api-key',
Expand Down
3 changes: 0 additions & 3 deletions backend/src/services/chat/__tests__/retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ const wallClockNow = performance.now.bind(performance)
const TEST_ENV = {
BACKEND_PORT: 3001,
NODE_ENV: 'test',
SUPABASE_DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/solarsim',
GOOGLE_API_KEY: 'google-api-key',
SUPABASE_URL: 'https://example.supabase.co',
SUPABASE_SERVICE_ROLE_KEY: 'service-role-key',
FRONTEND_URL: 'http://localhost:5173',
PDF_TOKEN_SECRET: 'test-secret-at-least-32-characters-long-ok',
GEMINI_API_KEY: 'gemini-api-key',
Expand Down
3 changes: 0 additions & 3 deletions backend/src/services/chat/__tests__/streamChat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
const TEST_ENV = {
BACKEND_PORT: 3001,
NODE_ENV: 'test',
SUPABASE_DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/solarsim',
GOOGLE_API_KEY: 'google-api-key',
SUPABASE_URL: 'https://example.supabase.co',
SUPABASE_SERVICE_ROLE_KEY: 'service-role-key',
FRONTEND_URL: 'http://localhost:5173',
PDF_TOKEN_SECRET: 'test-secret-at-least-32-characters-long-ok',
GEMINI_API_KEY: 'gemini-api-key',
Expand Down
2 changes: 1 addition & 1 deletion backend/src/services/overlayService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* single-band GeoTIFFs. The workbench renders these as semi-transparent PNG
* overlays on top of the satellite RGB image. This module converts a raw
* GeoTIFF band into a coloured PNG with the right alpha and dimensions, then
* caches the PNG in Supabase Storage so subsequent requests skip the work.
* caches the PNG in Cloudflare R2 so subsequent requests skip the work.
*
* Pipeline (per overlay request):
* 1. Try to return the cached `<tif>_overlay.png` via signed URL.
Expand Down
1 change: 0 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@shared/types": "workspace:*",
"@supabase/supabase-js": "^2.98.0",
"@tanstack/react-query": "^5.75.5",
"better-auth": "^1.6.25",
"class-variance-authority": "^0.7.1",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/api/locations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* A "Location" is the immutable Google Solar API record for a coordinate:
* building insights JSON, GeoTIFF storage paths, and the RGB satellite image.
* Each Location is fetched once and cached in Supabase, then reused by every
* Each Location is fetched once and cached in Postgres (Neon), then reused by every
* Project that references it.
*
* Endpoints:
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export type ProjectAnalysisConfig = StoredAnalysisConfigDto
*
* `rgbSignedUrl` and `imageGeoTransform` are populated only on the `/pdf-data`
* endpoint and let the PDF route render the panel layout over the satellite
* image without holding a Supabase session.
* image without holding an authenticated session.
*/
export type ProjectResponse = {
id: string
Expand Down Expand Up @@ -135,7 +135,7 @@ export type PdfExportToken = { token: string; expiresAt: string }

/**
* Mints a one-shot PDF token. Used because the headless PDF renderer (a
* Vercel function with no Supabase cookie) can't carry the user's session.
* Vercel function with no session cookie) can't carry the user's session.
*/
export function requestPdfExportToken(id: string) {
return apiFetch<PdfExportToken>(`/projects/${id}/pdf-token`, { method: 'POST' })
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/hooks/useLocale.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ function readInitialLocale(): SupportedLocale {
}

/**
* Provides the active UI locale and keeps it in sync with the Supabase user
* metadata so the choice survives across devices.
* Provides the active UI locale and keeps it in sync with the Better Auth user
* record so the choice survives across devices.
*/
export function LocaleProvider({ children }: { children: ReactNode }) {
const { user } = useAuth()
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/buildingInsights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ function getPanelCenter(raw: unknown): { lat: number; lng: number } | null {
* subset the frontend uses. Returns `null` when required fields (bounding box, panel dimensions,
* panel capacity) are missing or malformed; partial roof segments / panels with bad rows are dropped.
*
* @param raw - JSON value pulled from Supabase (`Location.buildingInsightsJson`)
* @param raw - JSON value pulled from Postgres (`Location.buildingInsightsJson`)
* @returns {@link ParsedBuildingInsights} or `null` when the payload is unusable
*/
export function parseBuildingInsights(raw: unknown): ParsedBuildingInsights | null {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ const resources = {
} as const

/**
* Type guard for locale codes coming from external sources (URL params, localStorage, Supabase user metadata).
* Type guard for locale codes coming from external sources (URL params, localStorage, Better Auth user record).
*
* @param value - Candidate locale string from an untrusted source
* @returns `true` when `value` is one of {@link SUPPORTED_LOCALES}, narrowing it to `SupportedLocale`
Expand Down
18 changes: 9 additions & 9 deletions frontend/src/locales/en/privacy.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"header": {
"title": "Data Privacy & Security",
"subtitle": "How SolarSim collects, uses, and protects your personal information under Malaysia's Personal Data Protection Act 2010 (PDPA).",
"lastUpdated": "Last updated: 5 May 2026"
"lastUpdated": "Last updated: 30 July 2026"
},
"tableOfContents": "On this page",
"sections": {
Expand All @@ -16,7 +16,7 @@
"dataCollected": {
"title": "Information We Collect",
"body": [
"We collect your account email and a password hash through Supabase Auth when you sign up with email and password. If you sign in with Google, Supabase also gives us the display name and email address attached to that account.",
"We collect your account email and a password hash when you sign up with email and password. Authentication runs inside the app itself, and these records are stored in our own database. If you sign in with Google, Google also gives us the display name and email address attached to that account.",
"We also collect project data you create or edit, including the project name, building coordinates, selected location status, edited solar panel layout, panel rotation and position, and the simulated billing inputs and saved analysis settings tied to that project. If you use the chat assistant, we also process the message you send and the project context needed to answer it."
]
},
Expand All @@ -37,15 +37,15 @@
"retention": {
"title": "How Long We Keep Your Data",
"body": [
"Project data stays in the system until you delete the project or delete your account. Account records from Supabase Auth remain as long as the account exists, because they are needed to authenticate you and protect the saved work linked to that account.",
"Project data stays in the system until you delete the project or delete your account. Account records remain as long as the account exists, because they are needed to authenticate you and protect the saved work linked to that account.",
"Shared location cache data may remain in the system to avoid repeated Solar API calls for the same coordinates, even after a project is removed. We keep operational logs and backup copies only for as long as needed for security, recovery, and service administration, and we do not intend to keep passwords, tokens, or other sensitive values in logs."
]
},
"sharing": {
"title": "Who We Share Information With",
"body": [
"We share data with processors that operate the app on our behalf. These include Supabase for Auth, PostgreSQL, and Storage; Heroku for backend hosting; Google for Solar API, Google Maps JavaScript API, and Vertex AI/Gemini; and Vercel for the browser-facing export or hosting service used by the app.",
"Supabase stores account records, project records, and uploaded or derived files in Storage, and may issue signed URLs so files can be fetched without making buckets public. Google receives the location and project context needed for mapping, solar analysis, and chat responses, and the relevant request data may be sent across borders as part of those services."
"We share data with processors that operate the app on our behalf. These include Neon for the PostgreSQL database; Cloudflare for R2 object storage; Heroku for backend hosting; Google for Solar API, Google Maps JavaScript API, Google Sign-In, and Vertex AI/Gemini; Resend for sending account emails; and Vercel for the browser-facing export or hosting service used by the app.",
"Neon stores account records and project records. Cloudflare R2 stores uploaded or derived files, and we issue signed URLs so those files can be fetched without making the bucket public. Resend receives your email address and the contents of account emails such as verification and password-reset messages. Google receives the location and project context needed for mapping, solar analysis, and chat responses, and the relevant request data may be sent across borders as part of those services."
]
},
"rights": {
Expand All @@ -58,21 +58,21 @@
"cookies": {
"title": "Cookies & Local Storage",
"body": [
"The app uses Supabase Auth session data in browser storage so you stay signed in, and it stores UI preferences in localStorage. Those preferences include locale, theme, project draft data, tour-dismissed flags, coverage-notice-dismissed, and workbench-hint-dismissed.",
"The app sets a secure, HTTP-only session cookie so you stay signed in. That cookie is used only to identify your session and cannot be read by scripts in the page. The app also stores UI preferences in localStorage, including locale, theme, project draft data, tour-dismissed flags, coverage-notice-dismissed, and workbench-hint-dismissed.",
"We do not use advertising cookies. Browser storage is used only to keep the app usable and to remember choices you make, and you can clear it through your browser settings if you want to sign out or reset saved preferences."
]
},
"security": {
"title": "How We Protect Your Data",
"body": [
"We protect traffic with TLS in transit. Passwords are handled by Supabase Auth as hashes, not as plain text, and our database access is restricted with row-level security so users can only reach their own records.",
"Storage files are delivered with signed URLs instead of public buckets, and we avoid logging passwords, tokens, or full sensitive payloads. No system is perfect, but we keep the implementation limited to the minimum data needed and use access controls at every layer we control."
"We protect traffic with TLS in transit. Passwords are stored as hashes, never as plain text, and every request to the API is checked against your session and scoped to your own records, so one account cannot reach another account's projects.",
"Stored files are delivered with signed URLs instead of public buckets, and we avoid logging passwords, tokens, or full sensitive payloads. No system is perfect, but we keep the implementation limited to the minimum data needed and use access controls at every layer we control."
]
},
"transfer": {
"title": "Cross-Border Data Transfer",
"body": [
"Most of the processors we use operate outside Malaysia, so your data may be transferred to and stored in other countries when you use the app. This includes the services run by Supabase, Google, Heroku, and Vercel.",
"Most of the processors we use operate outside Malaysia, so your data may be transferred to and stored in other countries when you use the app. This includes the services run by Neon, Cloudflare, Google, Heroku, Resend, and Vercel.",
"Under PDPA section 129, we take reasonable steps to make sure those recipients provide a level of protection that is reasonably comparable to Malaysian requirements for the data they process for us. We also limit the data sent to those services to what is needed for the feature you are using."
]
},
Expand Down
Loading
Loading