Architecture
Tech stack
- Frontend: Remix (React) + TypeScript + Vite, deployed on Vercel
- UI: shadcn/ui (Tailwind + Radix primitives), Lucide icons
- Backend: Remix
loader/actionfunctions + Supabase (Postgres, Auth, Storage) - AI: OpenAI (chat, assistants, vector stores) — wired via
app/.server/services/ai/ - Background workers: TypeScript jobs in
tasks/deployed to a VPS, talking to the same Supabase
Route layout
Routes live under app/routes/. The prefix convention controls auth and layout:
_in.*— authenticated app routes with the sidebar_out.*— public auth flows (login, signup, password reset)api.*— JSON endpoints invoked from the client or external services
Inside _in, the URL pattern is /{team-slug}/{feature-path}. The selected team is persisted in localStorage via useSelectedAccount and threaded through every loader as the active account_id.
Major feature areas:
| Path | Purpose |
|---|---|
/team/dashboard/* | Emissions analytics and overview |
/team/overview/* | Carbon accounting data entry by category |
/team/parcel/network/* | Parcel/logistics emissions tracking |
/team/write-texts/* | AI-assisted ESRS/sustainability report drafting |
Multi-tenancy
Built on Basejump, which provides:
basejump.accounts— teams and personal accounts (personal_account = truewhenid = user_id)basejump.account_user— membership + role (owner,member,super_admin)- Helper SQL functions (
basejump.has_role_on_account,basejump.get_accounts_with_role, …)
Every multi-tenant table in public is scoped by account_id and protected by RLS:
CREATE POLICY "Account members can access and modify" ON "table_name"
FOR ALL TO AUTHENTICATED
USING (account_id IN (SELECT basejump.get_accounts_with_role()))
WITH CHECK (account_id IN (SELECT basejump.get_accounts_with_role()));
Views must be SECURITY_INVOKER so RLS on underlying tables still applies:
ALTER VIEW "view_name" SET (SECURITY_INVOKER = ON);
Database schemas
| Schema | What lives here |
|---|---|
public | Main application tables — activities, emission factors, categories, accounting workflow |
basejump | Auth + multi-tenant primitives (accounts, members, roles, billing) |
parcel | Logistics network — orders, transporter nodes/edges, route emissions |
See data-model.md for entity relationships and schema/ for the per-table reference.
Frontend file organisation
app/
├── @/ # Shared infrastructure (kept @-prefixed so tsconfig paths resolve)
│ ├── components/ui/ # shadcn/ui primitives
│ ├── components/basejump/ # Auth-aware components
│ └── utils/supabase/ # Supabase clients (browser + server)
├── components/ # Domain components (categories, charts, dashboard, …)
├── routes/ # Pages — each file is a Remix route with loader/action
└── .server/ # Server-only code; never bundled to the client
@/ is the shared design system + framework glue. components/ is domain-specific UI. .server/ is anything that must never reach the browser (API keys, server-only services).
External services
| Service | What we use it for | Where it's configured |
|---|---|---|
| Supabase | DB, auth, storage | .env (SUPABASE_*), supabase/config.toml |
| OpenAI | Chat/assistants/vector stores | OPENAI_API_KEY, app/.server/services/ai/ |
| SendGrid | Transactional email | SENDGRID_API_KEY, EMAIL_SENDER |
| Mapbox | Maps + geocoding | MAPBOX_PUBLIC_KEY |
| Google Cloud Storage | Daily order CSV import | GOOGLE_* env vars + tasks/imported-list-* |
| Biluppgifter | Vehicle data lookups | BILUPPGIFTER_API_KEY |
Feature flags + admin access
- Set
account_user.account_roletosuper_adminon a user's personal account (row whereuser_id = account_id) to unlock admin tooling. - Account-level flags live in
accounts.public_metadata.feature_flags(string array).
Background tasks
The tasks/ directory holds standalone TypeScript services that run on a VPS, connect to the same Supabase, and process work the web app can't (long-running jobs, scheduled CSV imports, etc.). Each has its own README.md. Examples:
imported-list-processor— ingests orders from the daily CSVorder-relations-updater— continuously runsupdate_order_relations(~690 rows/min)geo-tagging— backfills address geometry