No description
  • TypeScript 99.8%
  • CSS 0.2%
Find a file
2026-05-12 16:40:50 -07:00
.claude Add workout plans, templates, and enhanced session management 2026-05-06 13:59:07 -07:00
.vscode Initial HealthMax scaffold — fitness tracker on Tawa 2026-05-04 15:33:21 -07:00
__tests__ Initial HealthMax scaffold — fitness tracker on Tawa 2026-05-04 15:33:21 -07:00
app Fix build: decouple bootstrap and session routes from old RecoveryLog schema 2026-05-12 16:40:50 -07:00
components Add nutrition tracking, recipe management, recovery logging, and weekly meal planning 2026-05-11 20:45:54 -07:00
lib Add nutrition tracking, recipe management, recovery logging, and weekly meal planning 2026-05-11 20:45:54 -07:00
public Add public/ directory (required by Next.js Dockerfile) 2026-05-04 15:43:57 -07:00
scripts Initial HealthMax scaffold — fitness tracker on Tawa 2026-05-04 15:33:21 -07:00
.env.example Initial HealthMax scaffold — fitness tracker on Tawa 2026-05-04 15:33:21 -07:00
.gitignore Initial HealthMax scaffold — fitness tracker on Tawa 2026-05-04 15:33:21 -07:00
catalog-info.yaml Restore catalog-version to 0.6.0 to fix OAuth provisioning 2026-05-06 12:25:27 -07:00
CLAUDE.md Initial HealthMax scaffold — fitness tracker on Tawa 2026-05-04 15:33:21 -07:00
next.config.ts Initial HealthMax scaffold — fitness tracker on Tawa 2026-05-04 15:33:21 -07:00
package.json Add HealthMax feature pages, Oura sync, AI coach, and user settings 2026-05-06 00:22:39 -07:00
pnpm-lock.yaml Add HealthMax feature pages, Oura sync, AI coach, and user settings 2026-05-06 00:22:39 -07:00
postcss.config.js Initial HealthMax scaffold — fitness tracker on Tawa 2026-05-04 15:33:21 -07:00
README.md Initial HealthMax scaffold — fitness tracker on Tawa 2026-05-04 15:33:21 -07:00
server.ts Fix preflight: catalog version 0.5.0, explicit /health handler 2026-05-04 15:33:49 -07:00
tsconfig.json Initial HealthMax scaffold — fitness tracker on Tawa 2026-05-04 15:33:21 -07:00
vitest.config.ts Initial HealthMax scaffold — fitness tracker on Tawa 2026-05-04 15:33:21 -07:00

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

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.local with 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
email 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:

  1. Clones your repo
  2. Generates a Dockerfile from your framework
  3. Builds and pushes the Docker image
  4. Provisions MongoDB and creates connection secrets
  5. Provisions an OAuth client via Bio-ID
  6. Provisions an S3 storage bucket
  7. Resolves internal dependencies to service URLs
  8. Deploys via Helm to Kubernetes
  9. Configures DNS (your-app.tawa.pro)
  10. 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_ID and BIO_CLIENT_SECRET into 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-ID
  • app/api/auth/callback/route.ts — exchanges code for tokens, sets cookies
  • app/api/auth/logout/route.ts — revokes token, clears cookies
  • app/api/auth/me/route.ts — returns current user as JSON
  • lib/auth.tsgetCurrentUser() with auto-refresh, requireAuth(), requireAdmin()
  • components/providers.tsxAuthProvider + 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/bioBio-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.tsgetSeptor() singleton
  • app/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/septorSeptor 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.tsgetRelay() singleton
  • lib/services/emailTemplates.tsbuildEmail() 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/relayRelay 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/storageStorage 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 in routes:
  • 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

  1. 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
  1. 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
}
  1. 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.


Documentation