Bundle Integration & Composition Guide
Version: 1.0.0
Status: Reference Documentation
Last Updated: November 13, 2025
Table of Contents
- Bundle Architecture Concepts
- How Bundles Work
- Dependency Resolution
- Type Safety & Consistency
- Creating Applications
- Deployment Scenarios
- API Patterns & Conventions
- Testing Bundles
- Versioning & Upgrades
- Troubleshooting
Bundle Architecture Concepts
What is a Bundle?
A bundle is a composable unit of functionality defined by:
- OpenAPI 3.0.1 Specification (single source of truth)
- Generated Backend (Spring Boot: entities, repos, services, controllers)
- Generated Frontend (TypeScript: RTK Query services, React hooks)
- Optional React Components (pre-built UI)
- Documentation (API docs, examples, usage guide)
Bundle Lifecycle
OpenAPI Spec (bundle.yaml)
↓ ThorAPI codegen
Generated Models & Services (Java + TypeScript)
↓ Hand-written business logic
Complete Bundle (ready to use)
↓ Bundle composing (selecting multiple bundles)
Merged OpenAPI Spec (all types + endpoints)
↓ Final codegen pass
Production-Ready Application
Bundle Maturity Levels
| Level | Description | Use Case |
|---|---|---|
| stable | Production-ready, proven in use | All production deployments |
| beta | Feature-complete, testing needed | Staging/early adopters |
| experimental | In development, APIs may change | Prototyping only |
| archived | No longer maintained | Don't use in new apps |
How Bundles Work
1. Bundle Definition (OpenAPI YAML)
Each bundle is a complete OpenAPI spec with:
openapi: 3.0.1
info:
title: "My Bundle"
version: 1.0.0
paths: {} # Can be empty for data-only bundles
x-valkyrai-dependencies:
- core.yaml # Declare what this bundle needs
- contentdata.yaml
components:
schemas:
MyModel:
type: object
x-valkyrai-service: microservice # IMPORTANT: enables auto-CRUD
properties:
id:
type: string
format: uuid
# ... more properties
Key Annotation: x-valkyrai-service: microservice
When ThorAPI sees this annotation, it auto-generates:
- Spring Data
Repositorywith QBE support - CRUD endpoints (
POST /v1/MyModel,GET /v1/MyModel/{id}, etc.) - TypeScript RTK Query service (
useGetMyModelsQuery(),useCreateMyModelMutation(), etc.)
2. Dependency Resolution
When you select bundles for an application:
application:
name: "My App"
bundles:
- core # Foundation
- onboarding-pro # Needs core
- workflow-embedded # Needs core
ThorAPI's bundle assembler:
- Detects dependencies (from
x-valkyrai-dependencies) - Validates dependency graph (no circular deps)
- Loads all specs in dependency order
- Merges schemas (deduplicates shared types)
- Generates merged OpenAPI (single source of truth)
Result: One complete OpenAPI spec with no duplication, perfect type consistency.
3. Code Generation
ThorAPI runs two passes:
Pass 1: Per-Bundle Generation
# Generate each bundle independently
thorapi bundles/core.yaml
thorapi bundles/onboarding-pro.yaml
thorapi bundles/workflow-embedded.yaml
Pass 2: Composite Generation
# Assemble all bundles into one spec
thorapi --compose \
--bundles core,onboarding-pro,workflow-embedded \
--output api-composite.yaml
Output:
api-composite.yaml- Merged OpenAPI spec (no dups)- Generated models, repos, controllers (all merged)
- Generated TypeScript clients (all services combined)
Dependency Resolution
Dependency Graph Format
Each bundle declares what it needs:
x-valkyrai-dependencies:
- core.yaml # Required (always first)
- contentdata.yaml # Optional
Build Order
ThorAPI automatically determines correct build order:
1. core (foundation, no deps)
2. contentdata (no deps)
3. onboarding-pro (depends: core)
4. compliance-pack (depends: core)
5. workflow-embedded (depends: core)
6. metrics-admin (depends: core)
7. digital-products-pro (depends: core, ecommerce)
8. docs-legal (depends: core, contentdata)
9. landing-page-builder (depends: core, contentdata)
10. marcomm, rating, admin (depends: core)
11. aurora-ui (no deps, always last)
Circular Dependency Detection
If you try to create a circular dependency:
# Bundle A
x-valkyrai-dependencies:
- bundle-b.yaml
# Bundle B
x-valkyrai-dependencies:
- bundle-a.yaml
ThorAPI rejects with error:
ERROR: Circular dependency detected!
Bundle A → Bundle B → Bundle A
Solution: Remove one of the dependencies
Shared Type Deduplication
When multiple bundles reference the same type:
# onboarding-pro.yaml
components:
schemas:
OnboardingWizardSession:
properties:
principalId:
type: string
format: uuid
# workflow-embedded.yaml
components:
schemas:
WorkflowExecution:
properties:
triggeredBy:
type: string
format: uuid
description: References Principal.id
Both bundles depend on Core, which defines Principal.
ThorAPI result:
Principaldefined once in composite spec (from core.yaml)- Both
OnboardingWizardSession.principalIdandWorkflowExecution.triggeredByreference samePrincipaltype - Zero duplication
- Perfect type consistency
Type Safety & Consistency
Single Source of Truth
Problem (Before Bundles):
Frontend:
interface Principal {
id: string;
email: string;
}
Backend:
class Principal {
private String id;
private String email;
}
Database:
CREATE TABLE principal (
id UUID,
email VARCHAR(255)
);
↓ Mismatch: email stored as 255 chars, but frontend expects string with no length
↓ Frontend validation passes, but backend validation fails
↓ Customer loses signup data
Solution (With Bundles):
OpenAPI Bundle (core.yaml):
Principal:
type: object
properties:
id:
type: string
format: uuid
email:
type: string
format: email
maxLength: 255
↓ ThorAPI generates:
Backend (Principal.java):
@Entity
class Principal {
@Id private String id;
@Column(length = 255) private String email;
}
Frontend (Principal.ts):
interface Principal {
id: string;
email: string;
}
// Validation:
const validEmail = (email: string) =>
email.length <= 255 && isValidEmail(email);
Database (liquibase/changelog.yaml):
- column:
name: email
type: VARCHAR(255)
✅ All three are identical
✅ No mismatches
✅ Perfect type safety
Validation Consistency
OpenAPI annotations translate to platform-specific validation:
components:
schemas:
User:
properties:
firstName:
type: string
minLength: 1
maxLength: 50
pattern: "^[a-zA-Z ]+$"
Generated Backend Validation (Java):
@Entity
class User {
@Column(length = 50)
@NotEmpty
@Pattern(regexp = "^[a-zA-Z ]+$")
private String firstName;
}
Generated Frontend Validation (TypeScript):
const userSchema = yup.object({
firstName: yup
.string()
.min(1, "Required")
.max(50, "Max 50 characters")
.matches(/^[a-zA-Z ]+$/, "Letters and spaces only"),
});
Generated Database Schema (Liquibase):
- column:
name: first_name
type: VARCHAR(50)
constraints:
nullable: false
All three validations are identical, so validation never fails asymmetrically.
Creating Applications
Step 1: Choose Bundles
# my-app.bundle-config.yaml
application:
name: "My Awesome App"
version: "1.0.0"
bundles:
- core # Foundation
- onboarding-pro # User acquisition
- digital-products-pro # Product management
- workflow-embedded # Automation
- metrics-admin # Analytics
- docs-legal # Help + compliance
- aurora-ui # Design system
Step 2: Run ThorAPI Bundler
# Generate merged OpenAPI spec
./thorapi-bundler compose \
--config my-app.bundle-config.yaml \
--output generated/
# Output:
# generated/
# ├── api-composite.yaml # Merged OpenAPI spec
# ├── backend/
# │ ├── java/
# │ │ └── com/valkyrlabs/generated/
# │ │ ├── model/ # @Entity classes
# │ │ ├── repository/ # Spring Data repos
# │ │ ├── service/ # Services
# │ │ └── controller/ # REST endpoints
# │ └── resources/
# │ └── openapi/
# │ └── api-out.yaml # Enhanced spec
# └── frontend/
# └── typescript/
# ├── api/ # RTK Query services
# │ ├── PrincipalService.ts
# │ ├── OnboardingFlowService.ts
# │ ├── WorkflowService.ts
# │ └── ...
# └── components/
# ├── OnboardingWizard.tsx
# ├── WorkflowBuilder.tsx
# └── ...
Step 3: Build Backend
# Spring Boot auto-detects generated entities
mvn clean install
# Result:
# - All entities registered with Spring Data JPA
# - All repositories created
# - All REST endpoints available
Step 4: Build Frontend
# Install generated services and components
npm install
# Result:
# - All RTK Query services available
# - useGetPrincipalsQuery()
# - useCreateOnboardingFlowMutation()
# - useGetWorkflowsQuery()
# - etc.
Step 5: Wire React App
import { useGetOnboardingFlowsQuery } from "@/api/OnboardingFlowService";
import { useGetWorkflowsQuery } from "@/api/WorkflowService";
import { OnboardingWizard, WorkflowBuilder } from "@/components";
export function App() {
const { data: flows } = useGetOnboardingFlowsQuery();
const { data: workflows } = useGetWorkflowsQuery();
return (
<>
<OnboardingWizard flows={flows} />
<WorkflowBuilder workflows={workflows} />
</>
);
}
Step 6: Deploy
docker build -t my-app:latest .
docker push my-app:latest
helm upgrade my-app ./chart/ --values values.yaml
Deployment Scenarios
Scenario 1: Microservices (Each Bundle = Service)
Frontend (React)
├── /v1/Principal → Principal Service
├── /v1/Onboarding → Onboarding Service
├── /v1/Workflow → Workflow Service
├── /v1/Dashboard → Metrics Service
└── /v1/LandingPage → Landing Page Service
Each service:
- Owns its bundle (models + endpoints)
- Has its own database
- Can scale independently
- Can deploy independently
Advantages:
- Independent scaling
- Deploy one service without others
- Different SLAs per service
Challenges:
- Inter-service calls (REST/gRPC)
- Distributed transactions
- Shared auth/identity
Scenario 2: Monolith (All Bundles = One App)
Frontend (React)
↓
Monolithic Spring Boot API
├── /v1/Principal (core)
├── /v1/Onboarding (onboarding-pro)
├── /v1/Workflow (workflow-embedded)
├── /v1/Dashboard (metrics-admin)
└── /v1/LandingPage (landing-page-builder)
↓
PostgreSQL (shared database)
Advantages:
- Simpler deployment
- Shared database (no distributed transactions)
- Easier debugging
- Single auth context
Challenges:
- Vertical scaling only
- One bad bundle can bring down all
- Longer CI/CD cycles
Scenario 3: Hybrid (Core + Specialized Services)
Monolith (Core Services):
├── /v1/Principal (core)
├── /v1/Onboarding (onboarding-pro)
└── Shared Auth Layer
Specialized Services (Separate):
├── Workflow Service (workflow-embedded)
├── Analytics Service (metrics-admin)
├── Compliance Service (compliance-pack)
└── Landing Page Service (landing-page-builder)
Frontend communicates with:
- Monolith for core endpoints
- Specialized services for specialized features
Advantages:
- Core is simple & stable
- Specialized features scale independently
- Easy to add/remove features
- Clear separation of concerns
API Patterns & Conventions
Endpoint Naming
All endpoints follow REST conventions:
GET /v1/{Resource} # List all
POST /v1/{Resource} # Create one
GET /v1/{Resource}/{id} # Get one
PATCH /v1/{Resource}/{id} # Update one
DELETE /v1/{Resource}/{id} # Delete one
GET /v1/{Resource}?example=... # Filter (QBE)
GET /v1/{Resource}/{id}/sub # Nested resource
POST /v1/{Resource}/{id}/action # Custom action
Examples:
GET /v1/Principal # List users
POST /v1/Principal # Create user
GET /v1/Principal/uuid # Get user
PATCH /v1/Principal/uuid # Update user
DELETE /v1/Principal/uuid # Delete user
GET /v1/Workflow # List workflows
POST /v1/Workflow # Create workflow
GET /v1/Workflow/uuid/execute # Execute workflow (custom action)
GET /v1/Dashboard/uuid/widgets # Nested: list widgets on dashboard
Request/Response Format
All endpoints use JSON:
// Request
{
"name": "My Workflow",
"description": "...",
"steps": [...]
}
// Response (Success)
{
"id": "uuid",
"name": "My Workflow",
"description": "...",
"steps": [...],
"createdDate": "2025-11-13T10:00:00Z",
"lastModifiedDate": "2025-11-13T10:05:00Z"
}
// Response (Error)
{
"error": "VALIDATION_ERROR",
"message": "Name is required",
"details": {
"field": "name",
"code": "REQUIRED"
},
"timestamp": "2025-11-13T10:00:00Z",
"path": "/v1/Workflow"
}
Query By Example (QBE)
Filter results without complex query syntax:
# Find workflows by name
GET /v1/Workflow?example=%7B%22name%22%3A%22Email%22%7D
# Decoded: ?example={"name":"Email"}
# Result: All workflows where name CONTAINS "Email" (case-insensitive)
# Find by organization
GET /v1/Workflow?example=%7B%22organizationId%22%3A%22uuid%22%7D
# Matching rules:
# - Strings: case-insensitive CONTAINS match
# - Numbers: exact match
# - Booleans: exact match
# - Dates: exact match (or range with operators)
# - UUIDs: exact match
Pagination
All list endpoints support pagination:
GET /v1/Workflow?page=1&limit=10
# Response:
{
"items": [...],
"page": 1,
"limit": 10,
"total": 47,
"pages": 5,
"hasNext": true,
"hasPrev": false
}
Sorting
Sort by any field:
GET /v1/Workflow?sort=name&order=asc
# Multiple sorts:
GET /v1/Workflow?sort=organizationId,createdDate&order=asc,desc
Testing Bundles
Unit Tests
Test bundle models:
@Test
public void testOnboardingFlowCreation() {
OnboardingFlow flow = new OnboardingFlow();
flow.setFlowName("Test Flow");
flow.setFlowType(FlowType.STANDARD);
assertEquals("Test Flow", flow.getFlowName());
assertTrue(flow.getSteps().isEmpty());
}
Integration Tests
Test bundle endpoints:
@SpringBootTest
class OnboardingFlowControllerTests {
@Autowired
private MockMvc mockMvc;
@Test
public void testCreateFlow() throws Exception {
OnboardingFlow flow = new OnboardingFlow();
flow.setFlowName("Test");
mockMvc.perform(post("/v1/OnboardingFlow")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(flow)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.flowName").value("Test"));
}
}
Composition Tests
Test multiple bundles together:
// React test
import { render, screen, waitFor } from "@testing-library/react";
import { OnboardingWizard } from "@/components/OnboardingWizard";
test("Onboarding + Workflow integration", async () => {
const { getByText } = render(<OnboardingWizard />);
// User completes onboarding
await userEvent.click(getByText("Get Started"));
// Workflow should be available after onboarding
await waitFor(() => {
expect(screen.getByText("Create Workflow")).toBeInTheDocument();
});
});
Versioning & Upgrades
Bundle Versioning
{
"id": "onboarding-pro",
"version": "1.0.0"
// 1 = major (breaking), 0 = minor (additive), 0 = patch (fixes)
}
Version Compatibility
| Change | Version Impact | Compatibility |
|---|---|---|
| Add new optional field | Patch 1.0.0 → 1.0.1 | ✅ Backward-compatible |
| Add new optional model | Minor 1.0.0 → 1.1.0 | ✅ Backward-compatible |
| Rename field | Major 1.0.0 → 2.0.0 | ❌ Breaking |
| Remove field | Major 1.0.0 → 2.0.0 | ❌ Breaking |
| Change field type | Major 1.0.0 → 2.0.0 | ❌ Breaking |
Upgrade Path
# Current application
bundles:
- onboarding-pro@1.0.0
- workflow-embedded@1.0.0
# Patch update (automatic)
bundles:
- onboarding-pro@1.0.1 # Bug fixes only
- workflow-embedded@1.0.0
# Minor update (review)
bundles:
- onboarding-pro@1.1.0 # New optional fields
- workflow-embedded@1.0.0
# Major update (plan migration)
bundles:
- onboarding-pro@2.0.0 # Requires code changes
- workflow-embedded@1.0.0
Migration Guide
When upgrading major versions:
# Migrating from onboarding-pro@1.x to 2.0
## Breaking Changes
1. `OnboardingWizardSession.formData` renamed to `collectedData`
- Update: `session.formData` → `session.collectedData`
2. `OnboardingStep.fields` changed from array to map
- Update: Loop over `Object.values(step.fields)`
3. `OnboardingCompletionResult` now requires `dashboardUrl`
- Update: Handle new required field in completion handler
## Migration Script
```bash
./migrate-onboarding-pro-1-to-2.sh
```
Rollback
If issues occur:
npm install onboarding-pro@1.4.0
---
## Troubleshooting
### "Bundle not found: marketing"
**Cause**: Bundle name misspelled or doesn't exist
**Solution**:
```bash
# List available bundles
./thorapi-bundler list
# Use correct name:
# - marketing → marcomm
# - docs → docs-legal
"Circular dependency detected"
Cause: Bundles depend on each other
Solution: Review dependencies, break circle
# Instead of:
# Bundle A → Bundle B → Bundle A
# Do:
# Bundle A → Shared Base
# Bundle B → Shared Base
"Type 'Principal' defined in multiple bundles"
Cause: Same type in multiple specs
Solution: Ensure all bundles that use Principal depend on core.yaml
x-valkyrai-dependencies:
- core.yaml # Must include this
"API endpoint not generated"
Cause: Model missing x-valkyrai-service: microservice annotation
Solution: Add annotation to model definition
components:
schemas:
MyModel:
type: object
x-valkyrai-service: microservice # ADD THIS LINE
properties:
id:
type: string
format: uuid
"RTK Query hook not available"
Cause: Frontend not regenerated after bundle changes
Solution: Rebuild frontend
npm run codegen # Regenerate from OpenAPI
npm install # Reinstall dependencies
npm run build
Summary
The bundle system enables:
✅ Single Source of Truth: OpenAPI defines all types
✅ Perfect Type Safety: Backend, frontend, DB always aligned
✅ Composability: Mix and match features
✅ Rapid Development: 80%+ auto-generated
✅ Enterprise Scale: Compliance, audit, permissions built-in
✅ Developer Velocity: Days instead of months
By understanding these concepts, you can:
- Create new applications in days
- Compose features with confidence
- Maintain consistency across teams
- Scale from startup to enterprise
Get building! 🚀
Document Version: 1.0.0
Status: Reference Documentation
Last Review: November 13, 2025
Next Update: Post-Phase 2 (after backend implementation)