Skip to content

Repository files navigation

Loomi

A smart-home dashboard: rooms and devices, scenes, schedules, if-this-then-that automations, energy cost analytics, household sharing, and a Gemini-powered assistant that controls the home in natural language. Real-time device state pushes over WebSocket.

There is no hardware layer — devices are simulated by a backend loop that drifts their state, records readings, and fires rules and schedules on each tick. Everything above that layer (auth, ownership, automation, billing, analytics) is real.

Frontend Next.js 16 (App Router, TS), Tailwind v4, shadcn/ui, next-themes
Backend FastAPI, WebSocket, google-genai (Gemini), Stripe
Database Supabase Postgres, schema owned entirely by Prisma (prisma-client-py) — no hand-written SQL
Auth Supabase Auth (email/password + TOTP 2FA); the backend verifies the access token per request
Deploy One Docker image (loomi-app) running both processes

Design decisions, feature history, and competitive research live in PLAN.md. ROADMAP-feasible-features.md is fully shipped (see its Implementation log); FEATURE-habit-learning.md is the one remaining unbuilt spec.


Prerequisites

  • Node 22+, Python 3.12+ (the container uses Node 22 + a venv)
  • A Supabase project (Postgres + Auth)
  • A Gemini API key — free from Google AI Studio
  • Optional: a Stripe account. Without it, /api/billing/* returns 503 and everyone stays on the Free plan; nothing else is affected.

Setup

1. Configure environment. Copy both examples and fill them in:

cp backend/.env.example  backend/.env
cp frontend/.env.example frontend/.env
  • backend/.env — SUPABASE_URL, SUPABASE_SERVICE_KEY, GEMINI_API_KEY, plus DATABASE_URL and DIRECT_URL.
  • frontend/.env — NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY (the anon/publishable key, never the service key), plus the API/WS URLs.

Two connection strings, and they are not interchangeable. DATABASE_URL must be Supabase's transaction-mode pooler (port 6543), which is IPv4-reachable — the direct host requires IPv6 egress the Docker container may not have. DIRECT_URL is the direct connection (port 5432) and is used only by prisma migrate / db execute, which the pooler can't serve.

2. Create the schema.

cd backend
python3 -m venv .venv && ./.venv/bin/pip install -r requirements.txt
./.venv/bin/prisma generate     --schema prisma/schema.prisma
./.venv/bin/prisma migrate deploy --schema prisma/schema.prisma
./.venv/bin/python prisma/seed.py     # optional demo home; idempotent

No SQL-editor step is needed. The seed builds a demo home (rooms, devices, an energy budget) owned by user.1@loomi.io.

3. Run it.

# Backend  → http://localhost:8000   (docs at /docs)
cd backend && ./.venv/bin/uvicorn app.main:app --reload

# Frontend → http://localhost:3000
cd frontend && npm install && npm run dev

Or the whole thing in one container:

docker compose up --build     # 3000 = app, 8000 = API

Compose mounts backend/.env rather than using env_file: — Compose interpolates $VAR patterns in env-file values, which silently corrupts any DB password containing a literal $. Both pydantic-settings and Prisma load a physical .env themselves.

4. Make yourself an admin. Sign up through the app, then in the Supabase SQL editor:

update profiles set role = 'admin' where email = 'you@example.com';

There is no self-service admin promotion, by design. Admins are redirected from /dashboard to /admin — an admin oversees members' homes rather than owning one.


How it fits together

Browser ──► Supabase Auth directly ............ sign-up / sign-in / session only
   │
   ├──► FastAPI :8000 ──► Prisma ──► Supabase Postgres ....... all app data
   │         │
   │         ├── verifies the caller's Supabase token per request (app/auth.py)
   │         └── simulator loop: drift state → write readings → broadcast
   │                            → evaluate rules → fire due schedules
   └──◄ WebSocket /ws/live ....................... live device-state push

The frontend never reads or writes tables. The anon key is used for Auth and nothing else; every row goes through the backend, which is why RLS is deliberately off (PLAN.md → Notes).

Ownership — the rule every router follows

Each member builds their own home. rooms, device_groups, scenes, schedules, automation_rules, and energy_budgets carry owner_id → profiles.id. devices deliberately does not — a device's owner is derived through device.room.owner_id (where={"room": {"owner_id": user.id}}).

Every member-facing endpoint filters lists by owner, validates referenced rows belong to the caller on create, and scopes where on update/delete. Acting on someone else's row returns 404, not 403, so existence isn't leaked either. Cross-member visibility exists only through admin.py. Two exceptions, both intentional: household.py (explicitly shared homes) and GET /api/activity (admins get the full log, members get only their own entries).

Adding an endpoint? It inherits none of this automatically — apply the pattern by hand.

Layout

backend/app/
  main.py  auth.py  db.py  config.py  schemas.py  ws.py
  simulator.py  rules_engine.py  schedules_engine.py  retention.py   # the tick loop
  ai.py  activity.py  billing.py  plans.py  geo.py  solar.py  readings.py  csv_export.py
  routers/   rooms devices rules scenes schedules groups notifications
             activity_log energy profile admin ai billing presence
             portability household access_codes
backend/prisma/    schema.prisma · migrations/ · seed.py
frontend/app/      (marketing)/ · dashboard/ · admin/ · account/
frontend/components/ · frontend/lib/

Two choke points worth knowing before changing behaviour: app/activity.py's notify() — every notification, quiet-hours check, and outbound webhook flows through it; and app/simulator.py's tick — rules, schedules, vacation mode, and the daily wellness check all hang off it, because the project has no cron infrastructure.


Conventions

  • Schema changes go through Prisma only. prisma migrate dev → a new folder under prisma/migrations/. No hand-written DDL, anywhere.
  • Nullable Json? columns: omit the key from the create() data dict entirely. Passing None raises MissingRequiredValueError — this shipped as a real bug once.
  • @db.Time columns need a full datetime (datetime.combine(EPOCH, t)) on write and .time() on read. A bare time is not serializable — also a real bug once.
  • Plan limits: call plans.check_limit() before creating, with the current count. It raises a structured 402 the frontend's UpgradeDialog parses.
  • Frontend type-check: cd frontend && npx tsc --noEmit.
  • Tests: cd backend && pytest (40 tests, no live DB — see backend/tests/fakes.py) and cd frontend && npm test (vitest). Both run in .github/workflows/ci.yml on push/PR, alongside the frontend type-check.

Known gaps

  • The command palette, device-offline UI, and voice dictation (see ROADMAP-feasible-features.md #6/#5/#9) were verified live via Playwright but have no committed browser-driven regression test — CI's frontend job is tsc --noEmit + vitest run (pure logic), not a browser suite.
  • Out of scope by decision: real hardware/Matter, transactional email, push notifications outside the app (webhooks cover it), and a first-class multi-home Home entity.

Future improvements

  • Habit Learning (suggested automations) — fully spec'd in FEATURE-habit-learning.md (schema, endpoints, frontend, scope boundaries) but status is explicitly "designed, not implemented." Next feature to build.
  • Browser-driven regression tests for the command palette, device-offline UI, and voice dictation — see "Known gaps" above. Logic/API is covered by CI; the live-verified UI flows are not yet automated.
  • Deployment — not yet hosted anywhere; not on the portfolio's project-deployments.md tracker at all, unlike the other 8 projects. Needs a hosting decision (Vercel + a backend host, Oracle VM, etc.) and the actual deploy.

About

Full-stack smart-home platform with real-time WebSocket device control, an if-this-then-that automation engine, and a Gemini-powered AI command bar.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages