Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7332a6e
feat: ship Stacklane v0.1.0 core API and v0.2.0 CLI/SDK
Abdulmuiz44 Jun 22, 2026
00ecfba
feat: ship Stacklane v0.1.0 core API and v0.2.0 CLI/SDK
Abdulmuiz44 Jun 22, 2026
244c741
feat: ship Stacklane v0.3.0 database credential hardening
Abdulmuiz44 Jun 22, 2026
f87a0e8
feat: ship Stacklane v0.4.0 storage and usage primitives
Abdulmuiz44 Jun 22, 2026
3637ec7
feat: ship Stacklane v0.4.0 storage and usage primitives
Abdulmuiz44 Jun 22, 2026
8227398
feat: add Stacklane storage and usage primitives
Abdulmuiz44 Jun 26, 2026
9591764
chore: remove generated build artifacts
Abdulmuiz44 Jun 26, 2026
6831010
chore: harden Stacklane v0.4.1
Abdulmuiz44 Jun 26, 2026
2173ba6
feat: add Stacklane MCP server
Abdulmuiz44 Jun 27, 2026
2df1dd1
feat: add Talocode Cloud prepaid billing
Abdulmuiz44 Jun 28, 2026
c564061
feat: add Stripe topups for Talocode Cloud
Abdulmuiz44 Jun 28, 2026
dfbf65e
chore: merge feat/stacklane-v0.4.0-storage-usage (ours strategy, supe…
Abdulmuiz44 Jun 28, 2026
4d14659
chore: merge feat/stacklane-v0.3.0 (ours strategy, superseded by main)
Abdulmuiz44 Jun 28, 2026
2d01df9
chore: merge feat/stacklane-v0.1-v0.2 (ours strategy, superseded by m…
Abdulmuiz44 Jun 28, 2026
5c164ae
feat: add Talocode Cloud billing dashboard UI with Radix/shadcn theme
Abdulmuiz44 Jun 29, 2026
15113c2
feat: add Talocode Cloud router
Abdulmuiz44 Jun 29, 2026
8ae49f4
fix: harden Talocode Cloud local validation
Abdulmuiz44 Jun 29, 2026
49673ff
fix: prepare Talocode Cloud demo flow
Abdulmuiz44 Jun 29, 2026
d348530
chore: adjust Talocode Cloud pricing
Abdulmuiz44 Jun 30, 2026
f906362
chore: standardize Talocode API base URL as TALOCODE_BASE_URL
Abdulmuiz44 Jun 30, 2026
a3f4f11
feat: add Talocode Cloud SDK client
Abdulmuiz44 Jun 30, 2026
6f8e6c8
feat: rename SDK surface to Talocode Cloud
Abdulmuiz44 Jun 30, 2026
e6c5c51
feat: add Codra Cloud SDK namespace
Abdulmuiz44 Jun 30, 2026
eb6f9ee
feat: add Talocode MCP server
Abdulmuiz44 Jun 30, 2026
73296a7
feat: add Talocode MCP bridge package
Abdulmuiz44 Jun 30, 2026
31866fe
feat: merge Codra Cloud API v0.1 + MCP bridge
Abdulmuiz44 Jun 30, 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
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
node_modules
.pnpm-store
.env
.env.*
!.env.example
.next
dist
coverage
.DS_Store
*.log
*.tsbuildinfo
apps/api/.migrations-cache
apps/dashboard/
.stacklane/
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Changelog

## 0.4.1 (MCP)

- added `@stacklane/mcp` package: open-source MCP server for Stacklane v0.4.1
- stdio transport for local agent clients (Codex, Claude Code, OpenCode, Cursor)
- 17 MCP tools: health, config, customers, API keys, usage, assets
- strict input schemas; raw API key returned only from `stacklane_create_api_key`
- key hashes never returned by list/revoke/verify tools
- asset tools reject unsafe filenames and path traversal
- secrets redacted from errors; no stack traces exposed
- local-first only; no Supabase, no Resend, no cloud provisioning, no billing
- added `docs/MCP.md`, `examples/mcp/*`, `scripts/test-stacklane-mcp-v010.mjs`
- added root `test:mcp` script

## 0.4.1

- add root `pnpm lint` via `scripts/lint-workspace.mjs`
- clarify that `apps/api/src/server.ts` is the active runtime entrypoint
- keep `apps/api/src/app.ts` as a compatibility shim for older checks
- add direct runtime endpoint coverage for customer, key, usage, asset, and file flows

## 0.4.0

- added local-first customers, API keys, usage events, and asset metadata primitives
- added `/api/v1/*` JSON endpoints for health, config, customers, keys, usage, and assets
- added local storage files under `.stacklane/`
- added SDK and CLI coverage for the new primitives
- added Talocode integration examples and storage/security docs

No billing, external platform dependency, or cloud provisioning is included in this release.
150 changes: 48 additions & 102 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,117 +1,63 @@
# Stacklane

Stacklane is a Nigeria-first, Africa-aware backend platform focused on helping teams ship production backends quickly with a reliable control plane.

## Current MVP Control-Plane Implementation
Stacklane currently runs as two apps:

- `apps/api` — TypeScript control-plane API backed by PostgreSQL
- `apps/web` — Next.js operator console

Implemented now:
- operator auth/session boundary (email/password login, session cookie, logout, current-user)
- organization/project membership-scoped access
- organizations, projects, environments, API keys, audit events
- provisioning orchestration foundation:
- async provisioning tasks and attempts
- retry/failure modeling
- region catalog
- runtime binding records
- mock provisioning adapter + in-process worker loop
- forward-only Postgres migrations + seed/bootstrap flow

## Provisioning model (current phase)
Provisioning lifecycle states:
- `queued`
- `running`
- `retrying`
- `ready`
- `failed`

Key runtime tables:
- `provisioning_tasks`
- `provisioning_attempts`
- `project_runtime_bindings`
- `regions`

Provisioning endpoints:
- `POST /projects/:idOrSlug/provision`
- `GET /projects/:idOrSlug/provisioning`
- `GET /projects/:idOrSlug/provisioning/tasks`
- `POST /projects/:idOrSlug/provisioning/retry`
- `GET /regions`

## Security model (current)
- Control-plane only auth (not app end-user auth)
- Passwords are hashed (scrypt)
- Session cookie (`sl_session`) stores opaque token; DB stores only token hash
- API keys store only hashed secret; raw secret is returned once at key creation

## Local development
### 1) Start Postgres
```bash
cd infra/docker
docker compose up -d
```
Stacklane is a lightweight backend/database/API layer for Talocode products.

### 2) Prepare API
```bash
cd apps/api
cp .env.example .env
npm install
DATABASE_URL=postgres://stacklane:stacklane@localhost:5432/stacklane npm run migrate
DATABASE_URL=postgres://stacklane:stacklane@localhost:5432/stacklane npm run seed
```
## Earlier Versions

### 3) Start API (includes provisioning worker loop)
```bash
cd apps/api
DATABASE_URL=postgres://stacklane:stacklane@localhost:5432/stacklane WEB_ORIGIN=http://localhost:3000 npm run dev
```
- v0.1.0: core API, access tokens, database connection storage, audit events, health endpoint
- v0.2.0: CLI, SDK, env generation, backup flow, token verification

### 4) Start web
```bash
cd apps/web
npm install
NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 npm run dev
```
## v0.4.0

Open `http://localhost:3000/signin` and sign in with seeded operator account:
- email: `admin@stacklane.local`
- password: `stacklane-admin`
Stacklane v0.4.0 is local-first.

## What is intentionally deferred
- real managed Postgres clusters / storage runtime / function runtime
- end-user auth for apps built on Stacklane
- billing provider integration
- advanced RBAC, SSO, MFA
- distributed queue infrastructure beyond current in-process worker baseline
- No external platform dependency
- No Supabase dependency
- No Resend dependency
- No billing yet
- API keys are hashed before storage
- Raw API keys are shown only once at creation
- File storage is local under `.stacklane/files/`

## Control-plane role policy
Current roles: `owner`, `admin`, `member`.
## Local Storage

Mutation policy baseline:
- `owner` / `admin`: project provisioning trigger/retry, API key create/revoke, environment create/update, project update
- `member`: read-only access to scoped organizations/projects and operational status surfaces
- `.stacklane/customers.json`
- `.stacklane/api-keys.json`
- `.stacklane/usage-events.json`
- `.stacklane/assets.json`
- `.stacklane/files/`

Policy logic is centralized in `apps/api/src/policy.ts` and enforced at API route boundaries.
## New v0.4.0 Primitives

## Provisioning retry + worker safety model
- Retry scheduling uses `next_run_at` with stepped backoff.
- Worker claims tasks with lease metadata (`claimed_by`, `claim_expires_at`) to reduce duplicate processing risk.
- Worker only picks runnable tasks where `next_run_at <= now()` and lease is free/expired.
- Failed tasks become terminal (`failed`) after `max_attempts`; manual retry endpoint is required to requeue.
- API customers
- API keys with `sk_lane_dev_...` and `sk_lane_live_...`
- Usage events and summaries
- Asset metadata records
- Local file persistence for hosted API workflows

## Tests
API test harness currently includes deterministic tests for:
- policy and permission matrix
- provisioning state transition rules
- retry backoff behavior
- provisioning formatter scheduling/lease contract
## Docs

- `docs/API.md`
- `docs/SDK.md`
- `docs/CLI.md`
- `docs/MCP.md`
- `docs/STORAGE_AND_USAGE.md`
- `docs/SECURITY.md`
- `docs/TALOCODE_INTEGRATION.md`

## MCP

Stacklane MCP v0.1 exposes Stacklane primitives as MCP tools for Codex, Claude Code, OpenCode, Cursor, and other MCP-compatible agents. Local-first, stdio transport, no cloud account required. See [docs/MCP.md](docs/MCP.md).

Run API tests:
```bash
cd apps/api
npm install
npm run test
pnpm --filter @stacklane/mcp build
node scripts/test-stacklane-mcp-v010.mjs
```

## Status

Future adapters may support object storage, but v0.4.0 does not require cloud provisioning or any external platform.

## License

MIT
165 changes: 165 additions & 0 deletions apps/api/migrations/0001_init_control_plane.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto;

DO $$ BEGIN
CREATE TYPE user_status AS ENUM ('active', 'invited', 'suspended');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

DO $$ BEGIN
CREATE TYPE organization_status AS ENUM ('active', 'suspended');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

DO $$ BEGIN
CREATE TYPE membership_role AS ENUM ('owner', 'admin', 'member');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

DO $$ BEGIN
CREATE TYPE membership_status AS ENUM ('active', 'invited', 'removed');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

DO $$ BEGIN
CREATE TYPE project_status AS ENUM ('provisioning', 'ready', 'failed', 'archived');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

DO $$ BEGIN
CREATE TYPE environment_kind AS ENUM ('production', 'development', 'preview');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

DO $$ BEGIN
CREATE TYPE environment_status AS ENUM ('active', 'disabled');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

DO $$ BEGIN
CREATE TYPE api_key_status AS ENUM ('active', 'revoked');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

DO $$ BEGIN
CREATE TYPE billing_status AS ENUM ('trial', 'active', 'past_due', 'canceled');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
name TEXT,
status user_status NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
status organization_status NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS organization_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role membership_role NOT NULL DEFAULT 'member',
status membership_status NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT organization_members_org_user_unique UNIQUE (organization_id, user_id)
);

CREATE INDEX IF NOT EXISTS organization_members_organization_id_idx ON organization_members(organization_id);
CREATE INDEX IF NOT EXISTS organization_members_user_id_idx ON organization_members(user_id);

CREATE TABLE IF NOT EXISTS projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
slug TEXT NOT NULL,
status project_status NOT NULL DEFAULT 'provisioning',
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT projects_org_slug_unique UNIQUE (organization_id, slug)
);

CREATE INDEX IF NOT EXISTS projects_organization_status_idx ON projects(organization_id, status);

CREATE TABLE IF NOT EXISTS environments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name TEXT NOT NULL,
kind environment_kind NOT NULL,
status environment_status NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT environments_project_name_unique UNIQUE (project_id, name)
);

CREATE INDEX IF NOT EXISTS environments_project_id_idx ON environments(project_id);

CREATE TABLE IF NOT EXISTS api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
name TEXT NOT NULL,
key_prefix TEXT NOT NULL,
hashed_key TEXT NOT NULL UNIQUE,
scopes JSONB NOT NULL DEFAULT '[]'::jsonb,
status api_key_status NOT NULL DEFAULT 'active',
last_used_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT api_keys_target_check CHECK (organization_id IS NOT NULL OR project_id IS NOT NULL)
);

CREATE INDEX IF NOT EXISTS api_keys_project_id_idx ON api_keys(project_id);
CREATE INDEX IF NOT EXISTS api_keys_organization_id_idx ON api_keys(organization_id);

CREATE TABLE IF NOT EXISTS usage_events (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
environment_id UUID REFERENCES environments(id) ON DELETE SET NULL,
event_type TEXT NOT NULL,
quantity NUMERIC(20,6) NOT NULL DEFAULT 1,
unit TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
occurred_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS usage_events_project_occurred_idx ON usage_events(project_id, occurred_at);
CREATE INDEX IF NOT EXISTS usage_events_organization_occurred_idx ON usage_events(organization_id, occurred_at);
CREATE INDEX IF NOT EXISTS usage_events_event_type_idx ON usage_events(event_type);

CREATE TABLE IF NOT EXISTS billing_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL UNIQUE REFERENCES organizations(id) ON DELETE CASCADE,
provider TEXT NOT NULL DEFAULT 'manual',
status billing_status NOT NULL DEFAULT 'trial',
currency TEXT NOT NULL DEFAULT 'NGN',
billing_email TEXT,
external_customer_ref TEXT UNIQUE,
current_plan TEXT NOT NULL DEFAULT 'free',
trial_ends_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Loading