SoloFrame: the platform layer behind an expanding AI portfolio.
Mono-PaaS is the modular-monolith repo; SoloFrame is the engine. DWA (verticals/dwa, PHI, MAIA-served) and GTM-OS (verticals/gtm, non-PHI, classifier: noop) are the shared-engine receipts. BizFrameHub and Colombian Spanish Academy now extend the evidence base: the same product discipline can move into SMB AI education and localized language learning without treating every new product as a fresh company.
The platform separates the engine from the verticals on purpose.
Four layers, each with a different audience and constraint. Verticals own brand and content. The engine owns primitives. Adapters own integration. Studio is the future self-serve surface, documented but not built.
A new academy should be a manifest and content problem, not a platform rebuild.
SoloFrame turns the reskilling wave into an implementation advantage: once the domain, brand, and curriculum are ready, the shared engine already owns auth, coaching, content serving, safety posture, LLM routing, onboarding, and deployment shape. Your branded academy, live in days, with an AI coach that knows your curriculum, and a safety layer you can dial up to clinical grade.
Studio
Self-serve vertical builder. User fills a form → engine generates a manifest → vertical deploys from the same Dockerfile. Documented in docs/architecture.md; not built. Ships only after Layer 3 has hardened across multiple vertical types.
Verticals
Branded apps. In the flagship Mono-PaaS repo, each vertical is thin: brand + content + manifest + Next 16 routes. Routes consume vertical-local lib/ shims; shims call into engine packages.
Expansion builds outside the flagship contrast now include BizFrameHub and Colombian Spanish Academy. They are proof surfaces for the platform operating model: package a domain, bind content to a product surface, deploy, measure, and harden what earns reuse.
Adapters
Per-integration packages at adapters/<name>/. Today: @svtech/adapter-nodebb (GTM forum). 13 named slots (flarum, r2-storage, resend, polar, stripe, brevo, attio, pgvector, etc.) live as vertical-local code awaiting promotion under the admission rule.
Engine packages
Sixteen @svtech/* packages. Each typed, Zod-validated, pnpm-workspace-resolved. Consumed via thin shim files in each vertical's lib/, never imported directly by routes.
What lands where, and what doesn't.
The engine stays small because two rules govern code placement. Both are documented as ADRs and enforced through review.
No vertical may fork an engine.
If a vertical needs behavior X, the engine declares X as a manifest knob exposed through the manifest schema. If you can't express X as a manifest knob, X doesn't belong in the engine yet. The change lands in packages/ as a primitive, not in the vertical as an override.
Carried forward as ADR-0001 from the predecessor solofame-platform attempt, which collapsed when forks accumulated.
≥2 consumers before engine entry.
Code lands in an engine package only when ≥2 verticals consume it OR a third real use case is committed. The lesson the predecessor attempt taught: an engine that absorbs single-vertical code by accident is a templated SaaS in a trench coat.
A primitive graduates from declared (in the manifest schema and engine factory surface) to load-bearing (consumed by ≥2 verticals through their lib/ shims) only when the second consumer arrives.
The structural difference between Mono-PaaS and a templated SaaS: a templated SaaS produces N forks of the same code with different config files; Mono-PaaS produces N runtime configurations of the same engine. DWA and GTM-OS are the shared-engine receipts. BizFrameHub and Colombian Spanish Academy show how the operating model generalizes into new market surfaces.
The platform thesis now has four public proof surfaces.
DWA and GTM-OS prove the hard shared-engine claim: opposite runtime behavior without a fork. BizFrameHub and Colombian Spanish Academy prove expansion velocity: the same product discipline can move into new audiences, content systems, localization needs, and go-to-market surfaces quickly.
Digital Wellness Academy
PHI-aware clinical education with MAIA-served safety, assessments, and provider coordination.
GTM-OS / SoloFrameHub
Founder GTM operating system with roleplay, execution AI, integrations, and the first GTM-OS localization proof in LatAm.
BizFrameHub
AI Business Academy for SMBs, professionals, consultants, and small teams.
Colombian Spanish Academy
Colombian Spanish learning vertical with Tutor Luz and the Bogotá Curada community guide.
Sixteen packages. Twelve wired into both flagship wiring files. Four wired selectively.
The DWA-and-GTM consumption pattern is what the admission rule was written to produce. The minimum-viable surface delivers the hard shared-engine claim - DWA + GTM-OS in production simultaneously, MAIA classifier sidecar serving, six-layer cost architecture operational. The expansion builds show what can be productized next without loosening the core.
| Package | What it owns | Consumption |
|---|---|---|
| @svtech/contracts | Zod schemas, shared types: VerticalManifestZ, Severity, content/lesson DTOs | both flagships |
| @svtech/manifest | defineManifest, validateManifest, computeManifestLock (canonical-JSON sha256) | both flagships |
| @svtech/auth | Lucia 3 factory, Argon2 helpers, session-cookie config, requireUser/getSession route helpers | both flagships |
| @svtech/db | Drizzle client factory, retry/timeout helpers, base column primitives. Vertical-specific tables stay in verticals/<v>/lib/db/schema.ts | both flagships |
| @svtech/redis | ioredis client factory + lazy connect (eager .connect() breaks Next 16 build-time prerendering) | both flagships |
| @svtech/safety | Classifier interface, defaultNoOpClassifier, createSafetyService with timeout + failSafe policy | both flagships |
| @svtech/coaching | Coach class, context-assembly primitives, prompt-layer composition, streaming wrappers, conversation state | both flagships |
| @svtech/content | MDX rendering primitives, lesson/course/module types, gray-matter frontmatter parsing, loadAllCourses loader | both flagships |
| @svtech/profile | Profile core type, ProfileRepository interface, Postgres + Mock implementations | both flagships |
| @svtech/onboarding | OnboardingFlow controller, persistence to profile, manifest-declared step ordering | both flagships |
| @svtech/presence | PresenceContext, manifest-declared signal providers per-vertical (recent_lessons, mood_log, pipeline_state, etc.) | both flagships |
| @svtech/logging | Structured logger factory, GlitchTip per-app DSN (Sentry-API-compatible; SDK unchanged, DSN swapped), audit-log writer. DWA's logger respects compliance.phi: true and drops free-text fields. | both flagships |
| @svtech/llm | Single OpenAI-SDK client pointed at OpenRouter, streamChat, token accounting, ModelRegistry | selective · GTM |
| @svtech/voice | OpenAI-direct Whisper STT + OpenAI TTS. The only place OpenAI is hit directly (OpenRouter doesn't proxy audio) | selective · on demand |
| @svtech/security | CSRF, sanitize (DOMPurify), encryption helpers, headers middleware, rate-limit primitives | selective · both |
| @svtech/http | Route helpers (with-auth, errors, request-context), React Query setup. Targeted route consumption. | selective · light |
Same TypeScript shape. Same Zod schema. Opposite runtime behavior.
The DWA-vs-GTM manifest diff is the cleanest demonstration the platform makes that compliance is a flag, not a fork. Every licensee question - "can your platform handle HIPAA?" "can it handle a non-clinical use case without paying for clinical safety overhead?" - has a one-character answer because of how the engine flexes.
| Field | DWA | GTM-OS | Engine effect |
|---|---|---|---|
| compliance.phi | true | (omitted, defaults false) | Logger drops free-text from request/response logs; retention enforced at 2557 days; audit log enabled for PHI-adjacent writes; engine refuses to boot without encryption-at-rest config |
| coaching.classifier | "maia" | "noop" | DWA's wiring.ts selects the MAIA classifier sidecar (sentinet/suicidality, ELECTRA, ~110M); GTM's selects defaultNoOpClassifier |
| coaching.guardrails | ["never-diagnose", "always-988-on-crisis", "no-medication-advice"] | ["no-financial-advice", "no-legal-advice"] | Same enforcement mechanism in @svtech/coaching; different list per manifest. Refused at the orchestration layer, not the model |
| forum.adapter | "flarum" | "nodebb" | Different adapter selected. Same forum primitive in the engine. DWA proxies via /community/*; GTM uses public forum URL via @svtech/adapter-nodebb |
| evals.datasets | ["coach_conversations", "distress_classifications"] | ["coach_conversations", "lesson_completions"] | Different captured signals per vertical; same eval framework. DWA's distress_classifications feed the MAIA five-stage retrain loop |
| features | { providerPortal, clinicalDataExport, moodTracking } | { roleplay, pods, badges } | Manifest-declared feature flags. Vertical routes/components gate rendering on these. New feature = manifest field + route check, not a fork |
export default defineManifest({
id: "dwa",
version: "1.0.0",
brand: { name: "Digital Wellness Academy", primaryColor: "#7c3aed" },
domain: "digitalwellness.academy",
compliance: {
phi: true,
retentionDays: 2557, // 7-year HIPAA-flavored window
},
coaching: {
classifier: "maia", // → services/maia/ sidecar wired in wiring.ts
guardrails: ["never-diagnose", "always-988-on-crisis", "no-medication-advice"],
contextSignals: ["recent_lessons", "mood_log", "thought_records"],
},
forum: { adapter: "flarum" },
features: { providerPortal: true, clinicalDataExport: true, moodTracking: true },
});computeManifestLock(manifest) hashes a canonical-JSON serialization with sha256 - two semantically equivalent manifests with different key order produce the same lock.
Routes never import @svtech/* directly.
A small set of vertical-side lib/ shim files thread vertical-specific config (Lucia user-attribute schemas, MAIA classifier wiring, OpenRouter model selection, Postgres pool config) into engine-package factories. When an engine package changes, the blast radius is the shim, not the routes.
lib/db/index.tsimport { createDbFactory } from "@svtech/db";
import { schema } from "./schema";
import { logger } from "../logger";
export const { db, withTenant } = createDbFactory(schema, { logger });
// Drizzle transaction with per-call tenant binding.
// v1: audited query paths through this contract; CI tenantLeakHarness
// blocks the build on cross-tenant visibility regression.
// v2: SET LOCAL ROLE platform_tenant; RLS policies key off the GUC.
await withTenant(ctx, async (tx) => {
// SET LOCAL app.tenant_id = <uuid>
return tx.select().from(schema.thoughtRecord).where(...);
});Audited at the application layer today. RLS-enforced in v2.
v1 enforces tenant scoping in application code with audited query paths and a CI tenant-leak harness. v2 moves enforcement to the database via Postgres row-level security keyed on a per-request GUC. The sequence is deliberate: ship the audited application-layer enforcement now, drive the v2 RLS rollout from a real regulated deployment.
Application-layer audited query paths
- →
withTenant(ctx, fn)Drizzle-transaction contract every DB-touching engine obeys - →Audited query paths through this single contract
- →CI
tenantLeakHarnessblocks the build on regression - →Lucia-3 sessions + role-based access on clinical-data routes
Database-layer row-level security
- →Postgres RLS policies keyed on per-request GUC (
app.tenant_id) - →Two roles, neither with
BYPASSRLS:platform_system+platform_tenant - →Tenant isolation moves from app-code to data boundary
- →Sequenced behind the marquee Stage-1 pilot landing
One adapter ships. Thirteen are aspirational, awaiting promotion.
Per the admission rule, an adapter graduates from vertical-local code to a shared adapters/* package only when the second consumer arrives or a third committed use case lands. Promotion is gated, not aspirational. The architecture document enumerates fourteen names; reality is that one has earned its package.
@svtech/adapter-nodebb
493 LOC including types, 101 LOC of tests. NodebbClient interface, createNodebbClient factory, full NodeBBCategory / Topic / Post / User types, tested write/read surface. GTM consumes through its lib/nodebb/client.ts shim - routes never fetch NodeBB directly.
Awaiting second consumer
Each maps to existing vertical-side code awaiting promotion when the second-consumer condition lands.
Most-likely-next-graduations: flarum (DWA forum), r2-storage (both verticals), resend (both verticals).
Modular monolith on Docker Swarm. Stateless replicas behind Traefik. Multi-node by design.
One Dockerfile parameterized by --build-arg APP=<vertical>. Three stages: deps, builder, runner. Both verticals deploy as Next 16 standalone images. Docker Swarm schedules across nodes; Traefik handles ingress and TLS; Dokploy is the control plane on top. The same code runs on one node today and scales horizontally by adding nodes - multi-node is a topology choice, not a code fork. Stateless replicas mean a failed node doesn't take the platform down, and rolling deploys don't require maintenance windows.
$ docker build -f infra/Dockerfile \
--build-arg APP=dwa \
-t mono-paas/dwa .
# Three stages:
# 1. deps - corepack-pinned pnpm 9, frozen-lockfile install
# 2. builder - pnpm --filter "./verticals/${APP}" build
# 3. runner - .next/standalone, HOSTNAME=0.0.0.0,
# docker-entrypoint.cjs runs migrations,
# exec node server.js$ git push origin main
# GitHub-App webhook → Dokploy control plane
# → application.redeploy on the affected vertical
# → Dockerfile rebuilds, Traefik routes new container
# Manual control via dk REST wrapper:
$ dk POST /api/application.redeploy \
'{"applicationId":"<id>"}'
# IDs pinned in infra/dokploy/state.jsondk REST wrapper for ops.postgres:16-alpine + GTM pgvector:pg16maia-mono:8001 on dokploy-networkExplicitly not in phase 1: Kubernetes, microservices, multi-region, per-tenant DB by default, custom AI gateway. Founder-scale infrastructure by choice.
Flagship contrast. Expansion proofs. Same platform discipline.
DWA and GTM-OS run on the Mono-PaaS engine today, with opposite compliance postures and zero forks. BizFrameHub and Colombian Spanish Academy show the same platform discipline expanding into new commercial and localized learning surfaces.