Module Registry
Open Saasframe keeps module metadata in a generated file rather than a dedicated REST endpoint. When you run yarn generate, the generator writes generated/modules.generated.ts, which exports the module graph consumed by the API dispatcher, navigation builders, and the OpenAPI generator.
import { modules, modulesInfo } from '@/generated/modules.generated'
modulesInfo.forEach((entry) => {
console.log(entry.id, entry.title)
})
const authApis = modules
.find((m) => m.id === 'auth')
?.apis?.map((api) => ('handlers' in api ? api.path : api.path))
Shapes and helpers
The exported types live in packages/shared/src/modules/registry.ts:
Module— top-level container withid, optionalinfo, arrays offrontendRoutes,backendRoutes,apis,features,customFieldSets, and more.ModuleApiRouteFile— shape for file-based API routes discovered underpackages/<pkg>/src/modules/<module>/api/**/route.ts; includespath,handlers, optional module-levelrequireAuth/requireRoles, and anopenApidescriptor if provided.ModuleRoute— metadata for frontend/back-office pages (pattern, guards, titles).ModuleInfo— optional descriptor exported from each module’sindex.ts(name, title, description, dependencies, etc.).
These structures are stable runtime contracts and can be imported anywhere in your application (Next.js server components, scripts, CLI tooling).
Enumerating APIs at runtime
import { modules } from '@/generated/modules.generated'
import type { ModuleApiRouteFile } from '@saasframe/shared/modules/registry'
const apiIndex = modules.flatMap((module) => {
return (module.apis ?? [])
.filter((api): api is ModuleApiRouteFile => 'handlers' in api)
.map((api) => ({
moduleId: module.id,
path: api.path,
methods: Object.keys(api.handlers),
requireAuth: api.metadata?.GET?.requireAuth ?? api.metadata?.POST?.requireAuth ?? false,
requireFeatures: api.metadata?.GET?.requireFeatures ?? api.metadata?.POST?.requireFeatures ?? []
}))
})
console.table(apiIndex)
- The array includes entries for every enabled module; modules without HTTP handlers (for example,
catalogat the time of writing) contribute an empty list. - Route-level
metadatamatches the per-method guards exported alongside the handler (packages/shared/src/modules/registry.ts:188), so you can surface RBAC hints in client SDKs. - The dispatcher in
src/app/api/[...slug]/route.tscallsfindApi(modules, method, pathname)to resolve the handler; your tooling can do the same for dry runs or static analysis.
Features and ACL seeding
Each module declares its feature flags in <module>/acl.ts. The generator hoists them into module.features, enabling tooling to seed default roles or audit coverage:
import { modules } from '@/generated/modules.generated'
const featureMatrix = modules.flatMap((module) =>
(module.features ?? []).map((feature) => ({
module: module.id,
feature: feature.id,
title: feature.title ?? feature.id
}))
)
Combine this with the Auth module’s GET /auth/features endpoint when you need a remote-friendly list; the HTTP route pads the same data with translations (packages/core/src/modules/auth/api/features.ts:5).
When to regenerate
- Run
yarn generatewhenever you add, rename, or delete APIs, pages, ACL declarations, or DI registrars. - The OpenAPI routes (
/api/docs/openapi,/api/docs/markdown) callbuildOpenApiDocument(modules, ...), so the explorer immediately reflects new endpoints. - The CLI scaffolding (
packages/cli/src/saasframe.ts) usesmodulesInfoto enable module-aware commands.
Rather than hitting a /modules HTTP endpoint, import the generated registry to inspect enabled modules, enumerate APIs, or reason about feature coverage. This keeps build artifacts deterministic while still giving you the structured metadata needed for automation.
For a human-readable list of every module that ships with Open Saasframe, plus the requires graph that ties them together and how it is enforced, see Module dependency graph.