Checkstack is built on a pluggable architecture that enables extensibility, modularity, and flexible deployment options. Everything beyond the core framework is implemented as a plugin, allowing the system to scale from monolithic deployments to distributed microservices.
Plugins MUST be registerable at runtime. This design enables:
Plugins register themselves with the core application through well-defined interfaces:
BackendPluginRegistryFrontendPlugin interfaceThe core calls plugin registration functions, not the other way around.
All plugin-to-plugin communication happens via:
This ensures security even in distributed deployments.
Each plugin is a standalone npm package that can:
checkstack/
├── core/
│ ├── backend/ # Core backend framework
│ ├── frontend/ # Core frontend framework
│ ├── backend-api/ # Backend plugin API
│ ├── frontend-api/ # Frontend plugin API
│ ├── common/ # Shared core types
│ ├── ui/ # Shared UI components
│ │
│ ├── auth-*/ # Authentication (essential)
│ ├── catalog-*/ # Entity management (essential)
│ ├── notification-*/ # Notifications (essential)
│ ├── healthcheck-*/ # Health monitoring (essential)
│ ├── queue-*/ # Queue abstraction (essential)
│ └── theme-*/ # UI theming (essential)
│
└── plugins/ # Replaceable providers only
├── auth-github-backend/ # GitHub OAuth provider
├── auth-credential-backend/ # Username/password auth
├── auth-ldap-backend/ # LDAP auth provider
├── queue-bullmq-*/ # BullMQ implementation
├── queue-memory-*/ # In-memory implementation
└── healthcheck-http-backend/ # HTTP health strategy
Note: See Packages vs Plugins Architecture for decision criteria on when to create a package vs a plugin.
Checkstack uses a strict package type system to maintain clean architecture:
| Package Type | Suffix/Pattern | Purpose | Can Depend On |
|---|---|---|---|
| Backend | -backend |
REST APIs, business logic, database | Backend packages, common packages |
| Frontend | -frontend |
UI components, pages, routing | Frontend packages, common packages |
| Common | -common |
Shared types, access rules, constants | Common packages only |
| Node | -node |
Backend-only shared code | Backend packages, common packages |
| React | -react |
Frontend-only shared components | Frontend packages, common packages |
These rules are automatically enforced by the dependency linter:
See dependency-linter.md for details.
Backend plugins use a two-phase initialization to ensure cross-plugin communication works correctly:
graph TD
A[Plugin Discovery] --> B[Load Plugin Module]
B --> C[Create Plugin Schema]
C --> D[Run Migrations]
D --> E[Call register function]
E --> F[Register Access Rules]
E --> G[Register Services]
E --> H[Register Extension Points]
E --> I[Register Init Function]
subgraph "Phase 2: Init"
I --> J[Resolve Dependencies]
J --> K[Call init - Register routers]
end
subgraph "Phase 3: After Plugins Ready"
K --> L[All Plugins Initialized]
L --> M[Call afterPluginsReady]
M --> N[Cross-plugin RPC + Hooks]
end
N --> O[Plugin Active]
Key Point: The
initfunction registers routers and services. TheafterPluginsReadycallback runs after ALL plugins have initialized, making it safe to:
- Call other plugins via RPC
- Subscribe to hooks (
onHook)- Emit hooks (
emitHook)
graph TD
A[Plugin Discovery] --> B[Load Plugin Module]
B --> C[Register APIs]
C --> D[Register Routes]
D --> E[Register Nav Items]
E --> F[Register Extensions]
F --> G[Plugin Active]
Each backend plugin gets its own isolated PostgreSQL schema:
Database: checkstack
├── Schema: public (core only)
├── Schema: plugin_catalog-backend
├── Schema: plugin_auth-backend
└── Schema: plugin_healthcheck-backend
See drizzle-schema-isolation.md for implementation details.
Extension points enable plugins to provide implementations for core functionality:
See extension-points.md for detailed documentation.
Plugins use versioned configurations to support backward compatibility:
interface VersionedConfig<T> {
version: number;
pluginId: string;
data: T;
migratedAt?: Date;
originalVersion?: number;
}
This enables:
See versioned-configs.md for details.
sequenceDiagram
participant F as Frontend Plugin
participant FA as Fetch API
participant R as Router
participant B as Backend Plugin
F->>FA: Request with credentials
FA->>R: HTTPS + JWT
R->>R: Validate JWT
R->>R: Check access
R->>B: Route to plugin
B->>R: Response
R->>FA: JSON response
FA->>F: Typed data
sequenceDiagram
participant P1 as Plugin A
participant S as Service Registry
participant P2 as Plugin B
P1->>S: Get service reference
S->>P1: Service instance
P1->>P2: Call via HTTPS + JWT
P2->>P2: Validate service token
P2->>P1: Response
Access rules are defined in common packages and registered by backend plugins:
// In catalog-common
export const access = {
entityRead: {
id: "entity.read",
description: "Read Systems and Groups",
},
} satisfies Record<string, AccessRule>;
// In catalog-backend
env.registerAccessRules(accessRuleList);
// In catalog-frontend
const canRead = accessApi.useAccess(access.entityRead.id);
The core automatically prefixes access rules with the plugin ID: catalog.entity.read
All plugins run in a single process:
bun run dev
Each plugin can run independently:
# Terminal 1
bun run dev:backend --plugins=catalog-backend
# Terminal 2
bun run dev:backend --plugins=auth-backend
# Terminal 3
bun run dev:frontend
Mix and match based on scaling needs: