Skip to main content

Bundle Integration & Composition Guide

Version: 1.0.0
Status: Reference Documentation
Last Updated: November 13, 2025


Table of Contents

  1. Bundle Architecture Concepts
  2. How Bundles Work
  3. Dependency Resolution
  4. Type Safety & Consistency
  5. Creating Applications
  6. Deployment Scenarios
  7. API Patterns & Conventions
  8. Testing Bundles
  9. Versioning & Upgrades
  10. Troubleshooting

Bundle Architecture Concepts

What is a Bundle?

A bundle is a composable unit of functionality defined by:

  1. OpenAPI 3.0.1 Specification (single source of truth)
  2. Generated Backend (Spring Boot: entities, repos, services, controllers)
  3. Generated Frontend (TypeScript: RTK Query services, React hooks)
  4. Optional React Components (pre-built UI)
  5. 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

LevelDescriptionUse Case
stableProduction-ready, proven in useAll production deployments
betaFeature-complete, testing neededStaging/early adopters
experimentalIn development, APIs may changePrototyping only
archivedNo longer maintainedDon'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 Repository with 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:

  1. Detects dependencies (from x-valkyrai-dependencies)
  2. Validates dependency graph (no circular deps)
  3. Loads all specs in dependency order
  4. Merges schemas (deduplicates shared types)
  5. 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:

  • Principal defined once in composite spec (from core.yaml)
  • Both OnboardingWizardSession.principalId and WorkflowExecution.triggeredBy reference same Principal type
  • 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

ChangeVersion ImpactCompatibility
Add new optional fieldPatch 1.0.0 → 1.0.1✅ Backward-compatible
Add new optional modelMinor 1.0.0 → 1.1.0✅ Backward-compatible
Rename fieldMajor 1.0.0 → 2.0.0❌ Breaking
Remove fieldMajor 1.0.0 → 2.0.0❌ Breaking
Change field typeMajor 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)