- TypeScript 99.8%
- CSS 0.2%
| .claude | ||
| .vscode | ||
| __tests__ | ||
| app | ||
| components | ||
| lib | ||
| public | ||
| scripts | ||
| .env.example | ||
| .gitignore | ||
| catalog-info.yaml | ||
| CLAUDE.md | ||
| next.config.ts | ||
| package.json | ||
| pnpm-lock.yaml | ||
| postcss.config.js | ||
| README.md | ||
| server.ts | ||
| tsconfig.json | ||
| vitest.config.ts | ||
Tawa Ecosystem App Scaffold
The official starting point for building apps on the Tawa platform. Fork this repo and you get a production-ready Next.js app with authentication, database, audit trail, email, file storage, and scheduled jobs — all wired up and deployable in minutes.
What's inside:
| Feature | How it works |
|---|---|
| Authentication | Bio-ID OAuth 2.0 + PKCE, auto-provisioned on deploy |
| Database | MongoDB via Mongoose, auto-provisioned on deploy |
| Audit Trail | Septor immutable event chain, fire-and-forget pattern |
| Email & SMS | Relay transactional messaging, fire-and-forget pattern |
| File Storage | S3-compatible object storage, auto-provisioned on deploy |
| Scheduled Jobs | iec-cron with example hourly sync job |
| Admin UI | Dashboard, data tables, modals, stats cards, status badges |
| Testing | Vitest with 80% coverage enforcement |
Prerequisites
Before you start, install the two CLI tools:
# Tawa CLI — deploy, configure, manage secrets
npm install -g tawa
# Koko — explore your databases through the Koko gateway
npm install -g kokopelli # installs the `koko` command
Create an account (opens Bio-ID in your browser):
tawa login
tawa whoami # confirm you're authenticated
Your account comes with 50,000 free gas tokens — enough for ~13 months of a nano pod.
Quick Start — Local Development
1. Clone and install
git clone https://git.insureco.io/insureco/healthmax.git my-app
cd my-app
pnpm install
2. Start with mock services (recommended)
The fastest way to get running locally — no real credentials, no Docker containers for platform services:
tawa dev --offline # generates .env.local + starts mock services on port 4500
pnpm dev # in a separate terminal — starts on http://localhost:4140
tawa dev --offline reads your catalog-info.yaml and:
- Generates
.env.localwith all platform env vars pre-filled for local mocks - Starts a mock server on port 4500 that fakes Bio-ID, Septor, Relay, and Storage
- The mock Bio-ID skips the login screen — clicking "Sign in" instantly authenticates as a dev user
Mock user identity (auto-authenticated on every login):
| Field | Value |
|---|---|
| bioId | BIO-DEV-USER-001 |
dev@localhost |
|
| name | Dev User |
| orgSlug | dev-org |
| roles | admin |
Where mock data goes (all in .tawa-dev/, gitignored):
septor-events.jsonl— Septor audit events (viewable at/api/admin/septor-trail)relay-messages.jsonl— emails and SMS (logged to terminal as they're sent)storage/— uploaded files organized by bucket
You still need MongoDB running locally. If you don't have it:
docker run -d -p 27017:27017 --name mongo mongo:7
2b. Manual setup (alternative)
If you prefer to configure manually or connect to real platform services:
cp .env.example .env.local
Edit .env.local with your local values:
# Database — install MongoDB locally or use Docker
MONGODB_URI=mongodb://localhost:27017/my-app-dev
# Bio-ID — get these from tawa oauth list after first deploy,
# or leave blank to skip auth locally
BIO_CLIENT_ID=
BIO_CLIENT_SECRET=
BIO_ID_URL=https://bio.tawa.insureco.io
# Platform services — leave blank for local dev (they're auto-injected in prod)
SEPTOR_URL=
# Storage — run MinIO locally with Docker (see below)
S3_HOST=localhost
S3_PORT=9000
S3_ACCESS_KEY_ID=minioadmin
S3_SECRET_ACCESS_KEY=minioadmin
S3_BUCKET=my-app-dev-default
# App
APP_URL=http://localhost:4140
PORT=4140
NODE_ENV=development
3. Start local services (optional)
If you need MongoDB and S3 locally (not needed for S3 when using tawa dev --offline):
# MongoDB
docker run -d -p 27017:27017 --name mongo mongo:7
# MinIO (S3-compatible storage) — only needed for manual setup
docker run -d -p 9000:9000 -p 9001:9001 --name minio \
minio/minio server /data --console-address ":9001"
4. Run the dev server
pnpm dev # starts on http://localhost:4140
The app runs on port 4140. Never use ports 3000-3010 — they're always taken.
Your First Deploy
1. Customize the scaffold
Open catalog-info.yaml — this is the single source of truth for your deployment:
metadata:
name: my-app # becomes my-app.tawa.pro
description: My application
annotations:
insureco.io/framework: nextjs
spec:
owner: your-org-slug # your org from tawa whoami
Change metadata.name to your service name and spec.owner to your org slug.
2. Run preflight checks
tawa preflight
This validates your catalog-info.yaml, health endpoint, git remote, and framework annotation. Fix any errors before deploying.
3. Push to Forgejo
# Create a new repo on Forgejo
tawa git repos create my-app
# Set the remote and push
git remote set-url origin https://git.insureco.io/your-org/my-app.git
git push -u origin main
4. Deploy
tawa deploy --prod --watch
The builder automatically:
- Clones your repo
- Generates a Dockerfile from your framework
- Builds and pushes the Docker image
- Provisions MongoDB and creates connection secrets
- Provisions an OAuth client via Bio-ID
- Provisions an S3 storage bucket
- Resolves internal dependencies to service URLs
- Deploys via Helm to Kubernetes
- Configures DNS (your-app.tawa.pro)
- Runs smoke tests
5. Verify
tawa status # check deployment status
tawa logs # stream live container logs
Your app is live at https://my-app.tawa.pro.
Platform Services — How They Work
Authentication (Bio-ID OAuth)
Authentication is auto-provisioned. When you declare auth.mode: sso in catalog-info.yaml, the builder:
- Creates an OAuth client in Bio-ID
- Injects
BIO_CLIENT_IDandBIO_CLIENT_SECRETinto your pod - Registers the callback URL:
https://{name}.tawa.pro/api/auth/callback
BIO_ID_URL is always injected as a core platform variable — no declaration needed.
The auth flow in this scaffold:
User clicks "Sign in" → /api/auth/login
→ Redirects to Bio-ID with PKCE challenge
→ User authenticates at Bio-ID
→ Bio-ID redirects to /api/auth/callback
→ App exchanges code for tokens
→ Stores tokens in httpOnly secure cookies
→ Redirects to /dashboard
Key files:
app/api/auth/login/route.ts— builds PKCE challenge, redirects to Bio-IDapp/api/auth/callback/route.ts— exchanges code for tokens, sets cookiesapp/api/auth/logout/route.ts— revokes token, clears cookiesapp/api/auth/me/route.ts— returns current user as JSONlib/auth.ts—getCurrentUser()with auto-refresh,requireAuth(),requireAdmin()components/providers.tsx—AuthProvider+useAuth()hook for client components
To protect a page or API route:
import { requireAuth } from '@/lib/auth'
export async function GET() {
const user = await requireAuth() // throws 401 if not logged in
return Response.json({ user })
}
SDK reference: @insureco/bio — Bio-ID SSO Integration Guide
Database (MongoDB)
Declare databases in catalog-info.yaml:
spec:
databases:
- type: mongodb # auto-injects MONGODB_URI
The builder provisions the connection string and creates a K8s secret. Your app reads from process.env.MONGODB_URI.
Key files:
lib/db.ts— Mongoose singleton connection (auto-reconnect, production exit-on-fail)lib/models/CronLog.ts— example Mongoose model
Adding a new model:
// lib/models/Customer.ts
import mongoose from 'mongoose'
const customerSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true },
orgSlug: { type: String, required: true },
}, { timestamps: true })
export const Customer = mongoose.models.Customer || mongoose.model('Customer', customerSchema)
Exploring your database with Koko:
npm install -g kokopelli # installs the `koko` command
koko databases # list all databases
koko collections my-app-prod # list collections
koko query my-app-prod customers --limit 5 # query documents
koko sample my-app-prod customers # see document shape
koko repl my-app-prod # interactive REPL
Koko proxies all queries through the Koko gateway — it never exposes raw connection strings. For direct access, use tawa db connect my-app --prod.
Supported database types: mongodb, redis, neo4j
Audit Trail (Septor)
Septor provides an immutable, hash-chained audit trail. Every financial, compliance, or authorization event MUST be recorded.
Declare septor as an internal dependency:
spec:
internalDependencies:
- service: septor # auto-injects SEPTOR_URL
Key files:
lib/services/septor.ts—getSeptor()singletonapp/api/admin/septor-trail/route.ts— admin route to query audit events
Emitting an event (ALWAYS fire-and-forget):
import { getSeptor } from '@/lib/services/septor'
const septor = getSeptor()
// CORRECT: fire-and-forget — never blocks your response
septor.emit('payment.created', {
entityId: orgSlug,
data: { amount: 5000, referenceId: paymentId },
metadata: { who: userId, why: 'Premium payment received' },
}).catch((err) => logger.error({ err }, 'Septor emit failed'))
// WRONG: never await septor on the request path
// await septor.emit(...)
SDK reference: @insureco/septor — Septor Integration Guide
Email & SMS (Relay)
Relay sends transactional emails and SMS. No catalog-info.yaml change needed — Relay is auto-accessible through Janus.
Key files:
lib/services/relay.ts—getRelay()singletonlib/services/emailTemplates.ts—buildEmail()HTML template helper
Sending email (ALWAYS fire-and-forget):
import { getRelay } from '@/lib/services/relay'
const relay = getRelay()
// Using a built-in template
relay.sendEmail({
template: 'welcome',
to: { email: user.email, name: user.name },
data: { firstName: user.firstName, orgName: org.name },
}).catch((err) => logger.warn({ err }, 'Welcome email failed'))
// Using custom HTML
relay.sendEmail({
content: {
subject: 'Your quote is ready',
html: '<h1>Hello healthmax</h1><p>Your quote is attached.</p>',
text: 'Hello healthmax, your quote is ready.',
},
to: { email: user.email, name: user.name },
data: { name: user.name },
options: { fromName: 'MyApp' },
}).catch((err) => logger.warn({ err }, 'Quote email failed'))
Sending SMS:
relay.sendSMS({
content: { text: 'Your code is {{code}}. Expires in 10 min.' },
to: { phone: '+15551234567' }, // E.164 format required
data: { code: '123456' },
}).catch((err) => logger.warn({ err }, 'SMS failed'))
Built-in templates: welcome, invitation, password-reset, magic-link
SDK reference: @insureco/relay — Relay Reference
File Storage (S3)
Declare storage buckets in catalog-info.yaml:
spec:
storage:
- name: default
tier: s3-sm # 1 GB, $2/month
The builder provisions the bucket and injects credentials automatically.
Using the SDK:
import { StorageClient } from '@insureco/storage'
const storage = StorageClient.fromEnv()
// Upload a file
const url = await storage.upload({
bucket: process.env.S3_BUCKET!,
key: 'documents/invoice-001.pdf',
body: pdfBuffer,
contentType: 'application/pdf',
})
// Generate a presigned download URL (1-hour TTL)
const downloadUrl = await storage.presign({
bucket: process.env.S3_BUCKET!,
key: 'documents/invoice-001.pdf',
expiresIn: 3600,
})
// List files
const files = await storage.list({
bucket: process.env.S3_BUCKET!,
prefix: 'documents/',
})
// Delete a file
await storage.delete({
bucket: process.env.S3_BUCKET!,
key: 'documents/invoice-001.pdf',
})
Storage tiers:
| Tier | Capacity | Cost/month |
|---|---|---|
s3-sm |
1 GB | $2 |
s3-md |
5 GB | $8 |
s3-lg |
25 GB | $30 |
s3-xl |
100 GB | $100 |
SDK reference: @insureco/storage — Storage Reference
Scheduled Jobs (iec-cron)
Declare schedules in catalog-info.yaml:
spec:
schedules:
- name: nightly-sync
cron: "0 2 * * *" # 5-part cron only (no seconds)
endpoint: /internal/cron/nightly-sync
timezone: America/Denver
timeoutMs: 60000
Implementing the endpoint — return 200 FIRST, then do work:
// app/internal/cron/nightly-sync/route.ts
export async function POST() {
// Respond immediately — never block iec-cron
const response = Response.json({ success: true })
// Do work async
doSync().catch((err) => console.error('Sync failed:', err))
return response
}
The scaffold includes a working example at app/internal/cron/example-sync/route.ts.
Key facts:
- Cron endpoints use
/internal/cron/*— never list these inroutes: - 5-part cron syntax only (no seconds field)
- Production: 5 gas tokens per execution. Sandbox: free.
- Cron fires via Janus — new schedules go live within 60 seconds of deploy
Secrets Management
All secrets are managed through the tawa CLI. Never hardcode secrets.
# Set an encrypted secret (injected on next deploy)
tawa config set STRIPE_KEY=sk_live_xxx --secret
# Set a plain config var (visible, not encrypted)
tawa config set LOG_LEVEL=debug
# List all config vars and secret names
tawa config list
# Pull everything to .env.local for local dev
tawa config pull
# Remove a config var
tawa config unset STRIPE_KEY
After setting secrets, you must deploy for changes to take effect. Database and OAuth secrets are provisioned automatically from catalog-info.yaml — you never set those manually.
Project Structure
app/
layout.tsx # Root layout with AuthProvider
globals.css # Tailwind 4 + CSS variables
page.tsx # Redirects to /dashboard
login/page.tsx # Bio-ID login page
health/route.ts # GET /health (required by platform)
(admin)/ # Protected admin routes
layout.tsx # Sidebar + Header shell
dashboard/page.tsx # Dashboard with StatsCards
api/
auth/ # login, callback, logout, me
admin/ # cron-logs, septor-trail
internal/
cron/ # iec-cron callback endpoints
components/
providers.tsx # AuthProvider + useAuth() hook
layout/
Sidebar.tsx # Collapsible nav with Lucide icons
Header.tsx # User info + logout button
DataTable.tsx # Generic sortable, paginated table
Modal.tsx # Escape key + backdrop close
StatsCard.tsx # KPI card with icon, label, value
StatusBadge.tsx # Status pill (active/pending/error/...)
EmptyState.tsx # No-data placeholder with action
LoadingSpinner.tsx # Animated spinner (sm/md/lg)
lib/
auth.ts # getCurrentUser, requireAuth, requireAdmin
api.ts # Client-side fetch wrapper (apiGet, apiPost, etc.)
config.ts # Typed environment config
db.ts # Mongoose singleton connection
utils.ts # cn(), formatCurrency(), formatDate()
services/
septor.ts # getSeptor() singleton
relay.ts # getRelay() singleton
emailTemplates.ts # buildEmail() HTML helper
models/
CronLog.ts # Example Mongoose model
__tests__/
setup.ts # Vitest environment setup
utils.test.ts # Utility function tests
db.test.ts # Database connection tests
Extending the Scaffold
Add a new page
Create a file under app/(admin)/:
// app/(admin)/customers/page.tsx
export default function CustomersPage() {
return <h1>Customers</h1>
}
Then add a nav entry in components/layout/Sidebar.tsx.
Add an API route
// app/api/customers/route.ts
import { requireAuth } from '@/lib/auth'
import { connectDB } from '@/lib/db'
import { Customer } from '@/lib/models/Customer'
export async function GET() {
const user = await requireAuth()
await connectDB()
const customers = await Customer.find({ orgSlug: user.orgSlug })
return Response.json({ success: true, data: customers })
}
Add a queue worker
- Declare in catalog-info.yaml:
spec:
queues:
- name: process-claim
endpoint: /internal/jobs/process-claim
concurrency: 5
retries: 3
internalDependencies:
- service: iec-queue # injects IEC_QUEUE_URL
- Implement the worker:
// app/internal/jobs/process-claim/route.ts
export async function POST(request: Request) {
const { jobId, data, attempt } = await request.json()
// Check idempotency, process, return 500 to retry
}
- Enqueue from your API:
await fetch(`${process.env.IEC_QUEUE_URL}/jobs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ namespace: 'my-app', queue: 'process-claim', data: { claimId } }),
})
Testing
pnpm test # run all tests
pnpm test:watch # watch mode
pnpm test:coverage # run with 80% coverage check
Coverage thresholds are enforced at 80% for statements, branches, functions, and lines.
CLI Quick Reference
# Auth
tawa login # authenticate via Bio-ID
tawa whoami # check current identity
# Deploy
tawa preflight # validate before first deploy
tawa deploy --prod --watch # deploy to production
tawa status # check deployment status
tawa logs # stream live container logs
# Config & Secrets
tawa config set KEY=val # set plain config var
tawa config set KEY=val --secret # set encrypted secret
tawa config list # list config vars
tawa config pull # download to .env.local
# Database
tawa db connect my-app --prod # get MongoDB connection string
koko databases # list databases via Koko
koko query my-db col # query collection
# Domains
tawa domain add my-app.com # point custom domain at your service
# OAuth
tawa oauth list # list OAuth clients
tawa oauth get <client-id> # show client details
# Troubleshooting
tawa troubleshoot # AI-powered diagnostics
tawa pods # check pod status
tawa restart # restart without rebuild
tawa rollback # roll back to previous deploy
Gas & Costs
Every Tawa account starts with 50,000 free tokens (1 token = $0.01).
| Resource | Cost |
|---|---|
| Nano pod (hosting) | 3,600 tokens/month ($36) |
| API call (default) | 1 token per successful call |
| Cron execution | 5 tokens (production only) |
| Storage (s3-sm) | 200 tokens/month ($2) |
Check your balance: tawa wallet
The deploy gate requires 3 months of hosting reserve before each deploy. For a nano pod, that's 10,800 tokens.