Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f4fd20b
Set default timezone for grid to 'UTC'
bobbybol Jul 16, 2026
9cc495c
Implement adjusted PostGIS db types
bobbybol Jul 16, 2026
a9d7d14
Introduce pino and supabase-js packages :: Write the supabase admin c…
bobbybol Jul 16, 2026
3e729e5
Export supabase from barrel
bobbybol Jul 16, 2026
f868026
Implement 'in-module' path aliases too with the '#' pattern
bobbybol Jul 16, 2026
0a80028
Logger module (pino)
bobbybol Jul 16, 2026
e5cb250
Implement foundational infrastructure modules and remove demo capability
bobbybol Jul 16, 2026
ba3a996
Seed file for local development
bobbybol Jul 16, 2026
037bbb2
Authentication + API testing
bobbybol Jul 16, 2026
f0bc16b
Add api-key strategy
bobbybol Jul 17, 2026
dd4cd17
Document some decisions on machine credentials
bobbybol Jul 17, 2026
c65e0cb
CORS + Validation Pipeline
bobbybol Jul 17, 2026
06d179e
Docs: Mark Task 7 (auth) as complete in migration plan
bobbybol Jul 17, 2026
1d6a9eb
Introduce and structure integration and e2e tests
bobbybol Jul 17, 2026
561c3c2
Import the user-admin module
bobbybol Jul 17, 2026
e93eb15
http tests for user-admin :: reintroduce ramda
bobbybol Jul 17, 2026
60fc186
Finally handle Cloudflare edge error comprehensively
bobbybol Jul 17, 2026
9fa8f07
Finalise import of foundation modules
bobbybol Jul 17, 2026
9ba7681
Wrap up foundation with a finished ADR for inter-host communication
bobbybol Jul 17, 2026
4382df7
Make supabase_id on accounts NOT NULL since one can't exist without t…
bobbybol Jul 18, 2026
0f8953d
Cleanup & nit fixes
bobbybol Jul 18, 2026
0ef2a31
Tell CodeRabbit to review any PR, on any branch
bobbybol Jul 18, 2026
2a50bb8
Minor documentation update
bobbybol Jul 18, 2026
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
6 changes: 6 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
reviews:
auto_review:
enabled: true
# Use ".*" to match all branches and review every PR regardless of target
base_branches:
- ".*"
24 changes: 24 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Shared workspace env for all apps
# Copy to `.env` at the repo root.
#
# Nx loads env files when you run targets (`nx serve api`, etc.):
# 1) apps/<host>/.env (host-specific — wins on conflicts)
# 2) this root .env (shared defaults)
# Production injects env via the platform (no .env files in the image).
# Secrets are validated per provider via requireEnv (ADR-007) — not dotenv-safe.
#
# Fill after `pnpm supabase start`. Local CLI may still label keys as
# anon / service_role in `supabase status`; our env names follow Supabase’s
# publishable / secret terminology (legacy JWT keys still work as values).

# --- Required (all apps) — admin / secret client --------------------------------
SUPABASE_URL="http://127.0.0.1:54321"
SUPABASE_SECRET_KEY=

# --- Logging (optional; used when nestjs-pino is restored) -------------------------
# LOG_LEVEL=info
# LOG_PRETTY=true

# --- Config artifact (optional; ADR-007) ------------------------------------------
# NXT_CONFIG_PATH=
# NXT_CONFIG_JSON=
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ coverage/
# Environment & secrets
.env
.env.*
!.env.example
!supabase/.env.example
!**/.env.example

# Supabase local state (root chain + legacy reference)
supabase/generated-types.ts
Expand Down
7 changes: 7 additions & 0 deletions .nxignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
# Frozen reference tree — not part of the active Nx workspace (see legacy/README.md)
legacy

# Non-runtime files — @nx/js:node watches the whole project when build is
# nx:run-commands; ignore these so `nx serve` does not restart on docs / httpYac edits.
docs
**/*.http
**/*.md
apps/*/http
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"recommendations": [
"dbaeumer.vscode-eslint",
"tombonnike.vscode-status-bar-format-toggle",
"firsttris.vscode-jest-runner"
"firsttris.vscode-jest-runner",
"anweber.vscode-httpyac"
]
}
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"editor.tabSize": 2,
"search.exclude": {
"package-lock.json": true,
"pnpm-lock.yaml": true,
"libs/*/migration": true,
"supabase/migrations": true
},
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ changes, or work clearly outside the domains below.
| Meter command batches, load shedding, meter grouping | 011 |
| Company cutover strategy | 012 |
| Capability vs core boundaries, behavior/module placement | 013 |
| Machine credentials — API keys, scopes, Postgres roles, MCP | 014 |

### How to read (progressive)

Expand Down
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
# NxtBackend

<a alt="Nx logo" href="https://nx.dev" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="45"></a>

✨ Your new, shiny [Nx workspace](https://nx.dev) is ready ✨.

[Learn more about this workspace setup and its capabilities](https://nx.dev/nx-api/js?utm_source=nx_project&amp;utm_medium=readme&amp;utm_campaign=nx_projects) or run `npx nx graph` to visually explore what was created. Now, let's get you up to speed!
# NXT Backend

## Generate a library

Expand Down
21 changes: 21 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Host-specific env for `api`.
# Copy to `apps/api/.env`.
#
# Shared secrets (`SUPABASE_URL`, `SUPABASE_SECRET_KEY`, LOG_*, NXT_CONFIG_*) live in
# the repo-root `.env` — see root `.env.example`. Nx merges: this file first, then root
# (first definition wins).

# --- Optional host port -----------------------------------------------------------
# PORT=3000

# --- Auth (api only — AuthModule / Passport strategies) -----------------------------
# Required when serving api. Validated via requireEnv in SupabaseStrategy (not worker).
# Publishable key (replaces legacy anon key) — user-scoped / RLS-exercising clients.
SUPABASE_PUBLISHABLE_KEY=
#
# Preferred JWT verification (asymmetric / JWKS). Local CLI:
SUPABASE_JWKS_URL="http://127.0.0.1:54321/auth/v1/.well-known/jwks.json"
#
# Legacy HS256 — used only when SUPABASE_JWKS_URL is unset.
# Both may be set; JWKS takes precedence (no runtime fallback if JWKS is unreachable).
# SUPABASE_JWT_SECRET=
18 changes: 18 additions & 0 deletions apps/api/http/.httpyac.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* httpYac environments — switch with the httpYac env picker in the status bar.
* Secrets come from parent `.env` files (`apps/api/.env` / repo root), same as `nx serve api`.
*/
module.exports = {
environments: {
$shared: {
baseUrl: 'http://localhost:3000',
supabaseUrl: 'http://127.0.0.1:54321',
platformEmail: 'superadmin@nxt-platform.com',
platformPassword: 'superadmin',
solarEmail: 'admin@nxt-solar.com',
solarPassword: 'admin',
devApiKey: 'dev-api-key-platform-superadmin',
},
local: {},
},
};
53 changes: 53 additions & 0 deletions apps/api/http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Local API smoke tests (httpYac)

File-based requests for the [httpYac](https://marketplace.visualstudio.com/items?itemName=anweber.vscode-httpyac) extension — `.http` files live in the repo next to the API.

## Setup

1. Install **httpYac** VS Code extension (`anweber.vscode-httpyac`) and disable any other http extension you might have.
2. Use the same env as `nx serve api` — httpYac loads `.env` from parent folders
(`apps/api/.env` / repo root). Needs at least `SUPABASE_PUBLISHABLE_KEY` for login.
3. In the editor status bar, select httpYac environment **`local`** (URLs + seeded users from
`.httpyac.js`).
4. Local Supabase + seed, and `pnpm exec nx serve api`.

## How to run

Open an endpoint file (e.g. `me.http`) and **Send** a request.
`# @import ./login.http` + `# @ref loginPlatform` runs the login first (cached until you
force-refresh), then the API call uses `{{loginPlatform.access_token}}`.

## Files

| File | Role |
|------|------|
| `.httpyac.js` | Shared local URLs + seeded emails/passwords |
| `login.http` | Named Supabase password grants (`loginPlatform`, `loginSolar`) |
| `me.http` | Imports login, refs it, calls `GET /auth/me` |
| `user-admin.http` | Task 9: create/update/delete customer, agent, member (+ API-key create-customer) |

## Seeded users

| Persona | Email | Password | Login name |
|---------|-------|----------|------------|
| Platform | `superadmin@nxt-platform.com` | `superadmin` | `loginPlatform` |
| Solar | `admin@nxt-solar.com` | `admin` | `loginSolar` |

New endpoint file pattern:

```http
# @import ./login.http

### Some endpoint
# @ref loginPlatform
GET {{baseUrl}}/…
Authorization: Bearer {{loginPlatform.access_token}}
```

## `user-admin.http` notes

- Seed already covers this: solar org **2**, grid **1**, platform API key. No seed change needed.
- Run requests **top-down** when a step needs a prior `# @name` id (create → update → delete).
- Creates real Auth users; timestamps keep emails unique. If Auth/phone collides, reset:
`pnpm exec supabase db reset`.
- Machine smoke: **Create customer (X-API-KEY)** — privileged admin path (no user client yet).
26 changes: 26 additions & 0 deletions apps/api/http/login.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Shared Supabase password-grant logins (seeded users).
# Imported by endpoint files via `# @import ./login.http` + `# @ref …`

@publishableKey = {{$dotenv SUPABASE_PUBLISHABLE_KEY}}

### Login — platform superadmin
# @name loginPlatform
POST {{supabaseUrl}}/auth/v1/token?grant_type=password
apikey: {{publishableKey}}
Content-Type: application/json

{
"email": "{{platformEmail}}",
"password": "{{platformPassword}}"
}

### Login — solar developer admin
# @name loginSolar
POST {{supabaseUrl}}/auth/v1/token?grant_type=password
apikey: {{publishableKey}}
Content-Type: application/json

{
"email": "{{solarEmail}}",
"password": "{{solarPassword}}"
}
15 changes: 15 additions & 0 deletions apps/api/http/me.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# @import ./login.http

### GET /auth/me (platform)
# @ref loginPlatform
GET {{baseUrl}}/auth/me
Authorization: Bearer {{loginPlatform.access_token}}

### GET /auth/me (solar)
# @ref loginSolar
GET {{baseUrl}}/auth/me
Authorization: Bearer {{loginSolar.access_token}}

### GET /auth/me (X-API-KEY — seeded platform key)
GET {{baseUrl}}/auth/me
X-API-KEY: {{devApiKey}}
118 changes: 118 additions & 0 deletions apps/api/http/user-admin.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# @import ./login.http
#
# Task 9 smoke — privileged user-admin (whole-method admin client).
# Needs: local Supabase + seed, `nx serve api`, httpYac env `local`.
# Creates Auth users + rows — use unique emails/phones (below) or `supabase db reset`
# between full runs if something collides.

### Create customer (solar bearer → grid 1)
# @name createCustomerSolar
# @ref loginSolar
POST {{baseUrl}}/user-admin/create-customer
Authorization: Bearer {{loginSolar.access_token}}
Content-Type: application/json

{
"full_name": "Smoke Customer",
"email": "smoke-customer-{{$timestamp}}@example.com",
"grid_id": 1,
"is_hidden_from_reporting": false
}

### Update customer (solar bearer)
# @ref loginSolar
POST {{baseUrl}}/user-admin/update-customer
Authorization: Bearer {{loginSolar.access_token}}
Content-Type: application/json

{
"id": {{createCustomerSolar.id}},
"full_name": "Smoke Customer Updated",
"email": "{{createCustomerSolar.account.email}}",
"is_hidden_from_reporting": true
}

### Create customer (X-API-KEY — machine path, platform key)
# @name createCustomerApiKey
POST {{baseUrl}}/user-admin/create-customer
X-API-KEY: {{devApiKey}}
Content-Type: application/json

{
"full_name": "Smoke Machine Customer",
"email": "smoke-machine-customer-{{$timestamp}}@example.com",
"grid_id": 1,
"is_hidden_from_reporting": false
}

### Create agent (solar bearer → grid 1)
# @name createAgentSolar
# @ref loginSolar
POST {{baseUrl}}/user-admin/create-agent
Authorization: Bearer {{loginSolar.access_token}}
Content-Type: application/json

{
"full_name": "Smoke Agent",
"phone": "+316{{$randomInt 10000000 99999999}}",
"email": "smoke-agent-{{$timestamp}}@example.com",
"grid_id": 1
}

### Update agent (solar bearer)
# @ref loginSolar
POST {{baseUrl}}/user-admin/update-agent
Authorization: Bearer {{loginSolar.access_token}}
Content-Type: application/json

{
"id": {{createAgentSolar.id}},
"full_name": "Smoke Agent Updated",
"phone": "+316{{$randomInt 10000000 99999999}}",
"email": "smoke-agent-updated-{{$timestamp}}@example.com"
}

### Invite member (solar bearer → org 2)
# @name inviteMemberSolar
# @ref loginSolar
POST {{baseUrl}}/user-admin/invite-member
Authorization: Bearer {{loginSolar.access_token}}
Content-Type: application/json

{
"email": "smoke-member-{{$timestamp}}@example.com",
"full_name": "Smoke Member",
"organization_id": 2,
"member_type": "TECH",
"redirectTo": "http://localhost:5173/"
}

### Update member (solar bearer — invited member)
# @ref loginSolar
POST {{baseUrl}}/user-admin/update-member
Authorization: Bearer {{loginSolar.access_token}}
Content-Type: application/json

{
"id": {{inviteMemberSolar.id}},
"full_name": "Smoke Member Updated",
"member_type": "TECH",
"training_level": 1,
"subscribed_to_telegram_revenue_notifications": false,
"hidden": false
}

### Delete customer created by solar bearer (cleanup)
# @ref loginSolar
DELETE {{baseUrl}}/user-admin/customer/{{createCustomerSolar.id}}
Authorization: Bearer {{loginSolar.access_token}}

### Delete agent (cleanup)
# @ref loginSolar
DELETE {{baseUrl}}/user-admin/agent/{{createAgentSolar.id}}
Authorization: Bearer {{loginSolar.access_token}}

### Delete invited member (cleanup — member id from invite response)
# @ref loginSolar
DELETE {{baseUrl}}/user-admin/member/{{inviteMemberSolar.id}}
Authorization: Bearer {{loginSolar.access_token}}
22 changes: 5 additions & 17 deletions apps/api/jest.config.cts
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
/* eslint-disable */
const { readFileSync } = require('fs')

// Reading the SWC compilation config for the spec files
const swcJestConfig = JSON.parse(
readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8')
);

// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves
swcJestConfig.swcrc = false;
const { shared } = require('./jest.shared.cjs');

/** Default `nx test api` — unit only (no local Supabase / stack). */
module.exports = {
...shared,
displayName: 'api',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['@swc/jest', swcJestConfig]
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: 'test-output/jest/coverage'
testMatch: [ '<rootDir>/test/unit/**/*.(spec|test).ts' ],
passWithNoTests: true,
};
9 changes: 9 additions & 0 deletions apps/api/jest.e2e.config.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const { shared } = require('./jest.shared.cjs');

/** `nx run api:test-e2e` — needs local Supabase + seed. */
module.exports = {
...shared,
displayName: 'api-e2e',
testMatch: [ '<rootDir>/test/e2e/**/*.(spec|test).ts' ],
passWithNoTests: true,
};
9 changes: 9 additions & 0 deletions apps/api/jest.integration.config.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const { shared } = require('./jest.shared.cjs');

/** `nx run api:test-integration` — needs local Supabase + seed. */
module.exports = {
...shared,
displayName: 'api-integration',
testMatch: [ '<rootDir>/test/integration/**/*.(spec|test).ts' ],
passWithNoTests: true,
};
Loading