Skip to main content

Application Marketplace

The marketplace feature provides complete application lifecycle management, enabling apps to be registered, installed, uninstalled, and managed within a multi-tenant environment.

Scope

This document covers the following packages and their interfaces:

LayerPackagesKey Files
Applicationpkg/v2/application/marketplace/marketplace_service.go, service.go, commands.go, responses.go, errors.go
Domain Servicespkg/v2/domain/service/dependency_resolver.go, route_conflict_detector.go, certificate_validator.go, asset_validator.go
Domain Modelspkg/v2/domain/app/, pkg/v2/domain/dependency/, pkg/v2/domain/lifecycle/, pkg/v2/domain/route/app.go, dependency.go, state_machine.go, registry.go
Infrastructurepkg/v2/infrastructure/authz/, pkg/v2/infrastructure/health/, pkg/v2/infrastructure/ratelimit/tuple_manager.go, checker.go, limiter.go
Repositoriespkg/v2/domain/repository/app_repository.go, dependency_repository.go, route_repository.go, asset_repository.go
Presentationpkg/v2/presentation/grpc/, pkg/v2/presentation/graphql/marketplace_server.go, GraphQL resolvers
Proto Definitionseasy.proto/v2/go/manifestmanifest.proto (settings schema)

Overview

Based on pkg/v2/application/marketplace/, the marketplace system orchestrates:

  • App Registration: Apps authenticate with certificates and declare capabilities via manifests
  • App Installation: Dependency resolution, route registration, and permission allocation
  • App Uninstallation: Graceful removal with impact analysis
  • Dependency Management: HARD and SOFT dependencies with cycle detection
  • Route Management: API/UI route registration with conflict detection
  • Permission Management: Fine-grained authorization via OpenFGA tuples
  • Lifecycle Events: Integration with event bus for state notifications

Architecture

Layered Structure

Presentation Layer (gRPC/GraphQL)

Application Layer (marketplace_service.go)

Domain Layer (app/, lifecycle/, dependency/)

Infrastructure Layer (repositories, events, routes)

Core Components

marketplaceService (pkg/v2/application/marketplace/marketplace_service.go:24-51)

type marketplaceService struct {
// Repositories
appRepo repository.AppRepository
assetRepo repository.AssetRepository
dependencyRepo repository.DependencyRepository
routeRepo repository.RouteRepository

// Domain services
stateMachine lifecycle.StateMachine
certValidator service.CertificateValidator
assetValidator service.AssetValidator
routeDetector service.RouteConflictDetector
dependencyResolver service.DependencyResolver

// Application services
settingsService settingsService

// Infrastructure
routeRegistry route.Registry
tupleManager *authz.TupleManager
rateLimiter ratelimit.Limiter
healthChecker health.Checker
eventBus event.Bus

// Telemetry
logger telemetry.Logger
}

Dependencies & Interactions:

  • → Settings Service (lines 54-58): Registers/validates app settings, deletes on deregistration
  • → Route Registry (in-memory): Fast lookup for proxy routing
  • → Route Repository (PostgreSQL): Persistent route storage
  • → Tuple Manager (OpenFGA): Permission grants/revokes
  • → Event Bus (RabbitMQ): Publishes lifecycle events
  • → Dependency Resolver: Validates dependencies, checks install preconditions
  • → Route Conflict Detector: Validates route patterns before registration
  • → Health Checker: Registers/unregisters app health monitors
  • → Rate Limiter: Configures API and per-route rate limits

Service Interface

Service (pkg/v2/application/marketplace/service.go:9-36)

type Service interface {
RegisterApp(ctx context.Context, cmd RegisterAppCommand) (*RegisterAppResponse, error)
InstallApp(ctx context.Context, cmd InstallAppCommand) (*InstallAppResponse, error)
UninstallApp(ctx context.Context, cmd UninstallAppCommand) (*UninstallAppResponse, error)
DeregisterApp(ctx context.Context, cmd DeregisterAppCommand) (*DeregisterAppResponse, error)
GetApp(ctx context.Context, query GetAppQuery) (*app.App, error)
ListApps(ctx context.Context, query ListAppsQuery) (*ListAppsResponse, error)
RestoreRoutesFromDB(ctx context.Context) error
}

App Registration Flow

Based on marketplace_service.go:100-270:

Process Steps

StepFunctionFile:LinesDescription
1Create app entitymarketplace_service.go:104app.NewApp() with certificate and manifest
2Create aggregatemarketplace_service.go:107-113Wrap app with validators and state machine
3Execute domain validationmarketplace_service.go:116agg.Register() validates certificate, assets
4Validate navigationmarketplace_service.go:124-135Check UI navigation structure (max 3 levels)
5Check existing appmarketplace_service.go:138-157Handle re-registration if app exists
6Validate dependenciesmarketplace_service.go:160-164Check dependency existence, detect cycles
7Persist appmarketplace_service.go:167-169appRepo.Create()
8Persist dependenciesmarketplace_service.go:172-178dependencyRepo.Create() for each
9Persist assetsmarketplace_service.go:181-188assetRepo.Store() for each asset
10Register settingsmarketplace_service.go:190-208settingsService.RegisterSettings() if present
11Grant permissionsmarketplace_service.go:210-221OpenFGA tupleManager.GrantPermission()
12Register routesmarketplace_service.go:224-252In-memory registry + database persistence
13Publish eventmarketplace_service.go:255-259eventBus.Publish(AppRegistered)

Re-registration Pattern

If app already exists (handleReregistration, lines 274-469):

  • Permission Sync (lines 295-322): Calculates diff using diffPermissions(), revokes old, grants new
  • Route Update (lines 324-375): Deregisters old routes from registry/DB, registers new ones
  • Certificate Update (lines 378-379): Updates app certificate and webapp
  • State Transition (lines 383-389): Transitions UNINSTALLED apps back to REGISTERED
  • Database Update (lines 392-394): Persists updated app
  • Dependency Sync (lines 397-406): Deletes old dependencies, creates new ones
  • Asset Sync (lines 408-419): Deletes old assets, stores new ones
  • Settings Sync (lines 422-452): Deletes old settings, registers new ones
  • Event Publishing (lines 455-458): Publishes AppRegistered event

Atomicity: Registry registration happens first, rolled back if database persistence fails (lines 243-245, 366-368)

Example Manifest

{
id: 'todos-app',
name: 'Todo Management',
version: '1.2.0',
dependencies: {
hard: [
{
appId: 'auth-service',
version: '>=2.0.0 <3.0.0'
}
],
soft: [
{
appId: 'analytics',
version: '>=1.0.0'
}
]
},
routes: [
{
pattern: '/api/apps/todos/**',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
scopes: ['todos:read', 'todos:write']
}
],
permissions: {
required: ['users:read'],
exposed: ['todos:read', 'todos:write']
}
}

App Installation Flow

Based on marketplace_service.go:504-645:

Process Steps

StepFunctionFile:LinesDescription
1Retrieve appmarketplace_service.go:506-519Find by AppID or AppName
2Validate dependenciesmarketplace_service.go:526-528dependencyResolver.CheckInstallPreconditions()
3Validate settingsmarketplace_service.go:531-548Check required settings are configured
4Get existing routesmarketplace_service.go:551-559Fetch for conflict detection
5Create aggregate & installmarketplace_service.go:562-575agg.Install() checks route conflicts
6Update databasemarketplace_service.go:578-580Persist INSTALLED state
7Configure route permissionsmarketplace_service.go:594-602OpenFGA SetRoutePermissions()
8Configure rate limitsmarketplace_service.go:605-607API-level and per-route limits
9Register health checksmarketplace_service.go:610-626Independent health monitoring
10Publish eventmarketplace_service.go:630-633eventBus.Publish(AppInstalled)

Dependency Resolution

From pkg/v2/domain/service/dependency_resolver.go:

CheckInstallPreconditions validates:

  • All HARD dependencies are in INSTALLED state
  • HARD dependencies are healthy (passing health checks)
  • Returns DependencyNotInstalledError or DependencyUnhealthyError if preconditions fail
  • SOFT dependencies only generate warnings

App Uninstallation Flow

Based on marketplace_service.go:648-776:

Process Steps

StepFunctionFile:LinesDescription
1Retrieve appmarketplace_service.go:650-668Find by AppID or AppName
2Check dependentsmarketplace_service.go:670-690Unless force flag set
3Impact analysismarketplace_service.go:671-689CheckUninstallImpact() - find HARD dependents
4Create aggregate & uninstallmarketplace_service.go:693-706agg.Uninstall() transitions state
5Update databasemarketplace_service.go:709-711Persist UNINSTALLED state
6Deregister routesmarketplace_service.go:714-752Remove from registry and database
7Unregister health checksmarketplace_service.go:742-751Stop health monitoring
8Publish eventsmarketplace_service.go:755-764AppUninstalled + PermissionInvalidate

Force Uninstall

When Force: true flag is set (lines 670-690):

  • Bypasses dependent check
  • Allows removal even with HARD dependents
  • May break dependent apps (use with caution)

Dependency Management

Based on pkg/v2/domain/service/dependency_resolver.go:

Dependency Types

HARD Dependencies

  • App CANNOT function without them
  • Must be INSTALLED before dependent can install
  • Uninstalling requires checking dependents
  • Blocks installation if missing

SOFT Dependencies

  • App CAN function without them
  • Provides enhanced features if available
  • Generates warnings if missing
  • Does not block installation

Dependency Data Structure

From pkg/v2/domain/dependency/dependency.go:

type Dependency struct {
AppID uuid.UUID // App with dependency
DependsOnAppID uuid.UUID // App being depended on
Kind Kind // HARD or SOFT
VersionConstraint string // Optional: ">=1.0.0 <2.0.0"
CreatedAt time.Time
}

Version Constraints

Follows semantic versioning:

  • 1.2.3 - Exact version
  • >=1.0.0 - Minimum version
  • >=1.0.0 <2.0.0 - Range
  • ~1.2.0 - Patch updates allowed (1.2.x)
  • ^1.2.0 - Minor + patch updates allowed (1.x.y)
  • Empty string - Any version

Cycle Detection Algorithm

Uses Depth-First Search (DFS) with recursion stack:

  1. Build dependency graph from all apps
  2. Start DFS from each unvisited node
  3. Track visiting path in recursion stack
  4. If node already in current path → cycle detected
  5. Returns entire cycle path for debugging
  6. Enforces max depth of 50 to prevent infinite loops

Example Cycle:

If App A depends on B, B depends on C, C depends on A:
DFS detects: [A → B → C → A]
Returns: CyclicDependencyError with full path

Installation Order

Uses Kahn's topological sort algorithm:

  1. Build reverse dependency graph
  2. Find nodes with no incoming edges
  3. Process queue, removing edges
  4. Output nodes in dependency order
  5. Ensures dependencies installed before dependents

Route Management

Based on pkg/v2/domain/route/:

Dual Registration Pattern

Routes registered in two places:

  1. In-Memory Registry (routeRegistry.Register())

    • Fast lookup for proxy routing
    • Restored from database on startup via RestoreRoutesFromDB() (lines 935-980)
    • Implementation: pkg/v2/domain/route/registry_inmem.go
    • Thread-safe: Uses sync.RWMutex for concurrent access
  2. Persistent Storage (routeRepo.Create())

    • Durability across restarts
    • Source of truth
    • PostgreSQL storage

Conflict Detection

Based on pkg/v2/domain/service/route_conflict_detector.go:

Pattern Types:

  • PatternTypeExact: /api/items (exact match)
  • PatternTypePrefix: /api/items/** or /api/items/*
  • PatternTypeParameterized: /api/:id/items
  • PatternTypeWildcard: /api/*/items
  • PatternTypeRegex: regex:^/api/.*

Conflict Rules:

  • Routes conflict only if HTTP methods overlap
  • Exact patterns conflict with overlapping prefixes
  • Parameter patterns conflict if same structure
  • All new routes checked against existing AND each other

Error Example:

type RouteConflictError struct {
ConflictingRoute string // "/api/todos/** conflicts with /api/todos/items"
}

RegisteredRoute Structure

From pkg/v2/domain/route/route.go:

type RegisteredRoute struct {
ID uuid.UUID // Unique route ID
AppID uuid.UUID // App owning route
AppName string
BasePath string // e.g., "/api/apps/todos"
UpstreamBaseURL string // http://todos-bff.svc:8080
StripBasePath bool // Remove BasePath before forwarding
IsPublic bool // Skip auth if true
RouteSpecs []*Spec // Fine-grained routing rules
Permissions []string // Required OpenFGA scopes
RateLimit *config.RateLimit // API-level limits
HealthCheck *config.HealthCheckConfig
}

Permission Management

Based on marketplace_service.go:210-221, 594-602:

Permission Sync During Re-registration

Uses diffPermissions() (lines 473-501):

func (s *marketplaceService) diffPermissions(oldPerms, newPerms []string) (toRevoke, toGrant []string)
  • Calculates permissions to revoke and grant using set difference
  • Prevents unnecessary OpenFGA tuple operations
  • Logged for audit trail (lines 297-301)

OpenFGA Integration

Registration (lines 210-221):

for _, perm := range cmd.WebApp.APIConfig.RequiredPermissions {
err := s.tupleManager.GrantPermission(ctx, cmd.Name, perm)
// Continues on error (non-blocking)
}

Installation (lines 594-602):

for _, routeSpec := range foundApp.WebApp.APIConfig.Routes {
routeID := fmt.Sprintf("%s%s", BasePath, Pattern)
err := s.tupleManager.SetRoutePermissions(ctx, routeID, routeSpec.Scopes)
}

Tuple Management

From pkg/v2/infrastructure/authz/tuple_manager.go:

  • GrantPermission(ctx, app, permission) - Creates OpenFGA tuple
  • RevokePermission(ctx, app, permission) - Deletes tuple
  • SetRoutePermissions(ctx, routeID, scopes) - Sets per-route scopes
  • Failures logged but non-blocking (don't fail operation)

Lifecycle Events

Based on pkg/v2/domain/event/app_events.go:

Event Types

  • app.registered - App authenticated and manifest provided
  • app.installed - App transitioned to INSTALLED state
  • app.uninstalled - App removed from INSTALLED state
  • app.deregistered - App completely removed from system
  • permission.invalidate - Permission cache invalidation event

Event Publishing

// After registration (marketplace_service.go:255)
evt := event.NewAppRegistered(newApp.ID, newApp.Name)
s.eventBus.Publish(ctx, evt)

// After installation (line 630)
evt := event.NewAppInstalled(foundApp.ID, foundApp.Name)
s.eventBus.Publish(ctx, evt)

// After uninstallation (lines 755, 761)
evt := event.NewAppUninstalled(foundApp.ID, foundApp.Name)
s.eventBus.Publish(ctx, evt)

// Permission cache invalidation (line 761)
invalidationEvt := event.NewPermissionInvalidateEvent(...)
s.eventBus.Publish(ctx, invalidationEvt)

Non-Blocking Pattern:

  • Event publishing failures logged as warnings
  • Operations succeed even if events not published
  • Prevents marketplace from blocking on event bus

Commands and Responses

Commands

From pkg/v2/application/marketplace/commands.go:

type RegisterAppCommand struct {
Name string
Certificate *app.Certificate
WebApp *app.WebApp
Dependencies []*dependency.Dependency
SettingsSchema *pb.SettingsSchema
}

type InstallAppCommand struct {
AppID uuid.UUID
AppName string // Alternative to AppID
}

type UninstallAppCommand struct {
AppID uuid.UUID
AppName string // Alternative to AppID
Force bool // Force uninstall even if dependents exist
}

type DeregisterAppCommand struct {
AppID uuid.UUID
AppName string // Alternative to AppID
}

Responses

From pkg/v2/application/marketplace/responses.go:

type RegisterAppResponse struct {
AppID uuid.UUID
App *app.App
EventsEmitted []string
}

type InstallAppResponse struct {
AppID uuid.UUID
App *app.App
RoutesAdded int
EventsEmitted []string
}

type UninstallAppResponse struct {
AppID uuid.UUID
App *app.App
RoutesRemoved int
EventsEmitted []string
}

Error Handling

From pkg/v2/application/marketplace/errors.go:

// AppAlreadyExistsError - app already registered (lines 9-16)
type AppAlreadyExistsError struct {
Name string
}

// InvalidCommandError - command validation failure (lines 18-25)
type InvalidCommandError struct {
Reason string
}

// InvalidStateError - operation invalid for current state (lines 27-35)
type InvalidStateError struct {
CurrentState app.State
Reason string
}

// HasDependentsError - cannot uninstall app with dependents (lines 37-45)
type HasDependentsError struct {
AppName string
Dependents []string // Names of dependent apps
}

Error Handling Patterns

Non-Blocking Failures (don't abort operation):

  • Permission grants/revokes (lines 213-220, 304-322, 314-321)
  • Event publishing (lines 256-259, 631-633, 756-758, 762-764)
  • Health check registration/unregistration (lines 617-626, 743-750)
  • Asset/dependency deletion during re-registration (lines 398-399, 410-411)

Blocking Failures (abort operation):

  • App persistence errors (lines 167-169, 392-394)
  • Dependency validation failures (lines 161-164, 526-528)
  • Route conflicts (line 570)
  • Route registry/repository errors with rollback (lines 237-245)

GraphQL API

Based on pkg/v2/presentation/graphql/schema/:

Queries

type Query {
app(name: String!): App
apps(filter: AppFilter, first: Int, after: String): AppConnection!
}

input AppFilter {
state: AppState
states: [AppState!]
nameContains: String
registeredAfter: DateTime
installedAfter: DateTime
}

enum AppState {
UNREGISTERED
REGISTERED
INSTALLED
UNINSTALLED
}

Mutations

type Mutation {
installApp(name: String!): AppMutationResult!
uninstallApp(name: String!): AppMutationResult!
}

type AppMutationResult {
success: Boolean!
app: App
error: Error
}

Types

type App {
id: ID!
name: String!
version: String!
state: AppState!
dependencies: [AppDependency!]!
routes: [RouteSpec!]!
permissions: PermissionConfig!
registeredAt: DateTime!
installedAt: DateTime
}

type AppDependency {
appId: String!
kind: DependencyKind!
versionConstraint: String
}

enum DependencyKind {
HARD
SOFT
}

State Transitions

Valid transitions from pkg/v2/domain/lifecycle/state_machine.go:

FromToTrigger
UNREGISTEREDREGISTEREDApp connects and provides manifest
REGISTEREDINSTALLINGUser/admin requests installation
INSTALLINGINSTALLEDInstallation succeeds
INSTALLINGFAILEDInstallation fails (dependencies, conflicts)
INSTALLEDUNINSTALLINGUser/admin requests uninstall
UNINSTALLINGUNINSTALLEDUninstall succeeds
FAILEDINSTALLINGRetry installation
UNINSTALLEDINSTALLINGReinstallation
UNINSTALLEDREGISTEREDRe-registration updates manifest (line 383)

Concurrency Patterns

Thread Safety

Route Registry (pkg/v2/domain/route/registry_inmem.go):

type inMemoryRegistry struct {
mu sync.RWMutex // Protects routes map
routes map[uuid.UUID]*RegisteredRoute
}
  • Read operations: RLock() for concurrent reads
  • Write operations: Lock() for exclusive writes
  • Used in hot path (every request routing)

Marketplace Service:

  • Stateless service design
  • No internal mutable state
  • Thread-safe through repository/registry locking
  • Event bus handles concurrent event publishing

Atomic Operations

Route Registration with Rollback (lines 237-246):

// Register in in-memory registry first
if err := s.routeRegistry.Register(ctx, registeredRoute); err != nil {
return nil, fmt.Errorf("failed to register routes in registry: %w", err)
}

// Persist to database
if err := s.routeRepo.Create(ctx, registeredRoute); err != nil {
// Rollback in-memory registration on DB failure
_ = s.routeRegistry.Deregister(ctx, newApp.ID)
return nil, fmt.Errorf("failed to persist routes to database: %w", err)
}

Database Transactions:

  • Repository operations wrapped in transactions
  • Rollback on any step failure
  • Ensures consistency across app, dependencies, assets, settings

Design Patterns

1. Dual Registration (In-Memory + Database)

Why: Fast lookups in-memory + durability across restarts Implementation: RouteRegistry (in-mem) + RouteRepository (DB) Consistency: Registered to registry first, rolled back if DB fails Startup: RestoreRoutesFromDB() reloads registry from database (lines 935-980)

2. Non-Blocking Error Handling

  • Permission grants, event publishes, health checks fail non-blocking
  • Service continues even if these fail
  • Failures logged as warnings
  • Prevents cascade failures from external services

3. Re-registration Pattern

  • Existing apps can be re-registered with updated manifests
  • Atomic permission sync (revoke old, grant new)
  • Route updates (deregister old, register new)
  • Smooth transition from UNINSTALLED back to REGISTERED
  • Full sync of dependencies, assets, settings

4. State Machine with Validators

  • Each state transition has pre/post validators
  • Validators called before state change
  • Rollback on validator failure
  • Self-transitions allowed (idempotent)

5. Command-Query Separation

  • Commands: RegisterApp, InstallApp, UninstallApp, DeregisterApp
  • Queries: GetApp, ListApps
  • Clear separation of write and read operations

Security Features

  1. Certificate Validation - X.509 certificates required, validity checked
  2. Asset Signature Verification - Assets must be signed with app's certificate
  3. App Identity Validation - Cannot register manifest for another app
  4. Permission Isolation - Apps only receive their own lifecycle events
  5. OpenFGA Integration - Fine-grained permission checks via tuples
  6. Rate Limiting - Configurable per-route and API-level limits
  7. Health Checks - Independent health monitoring per app
  8. Force Uninstall Guard - Requires explicit flag to bypass dependent checks

Code Reference Table

ComponentFileLinesTests/VerificationDescription
Service Interfacemarketplace/service.go9-36integration/marketplace_service_test.go:61-150Main service contract
MarketplaceService Constructormarketplace/marketplace_service.go60-97integration/marketplace_service_test.go:34-59Service initialization with 15+ dependencies
RegisterAppmarketplace/marketplace_service.go100-270integration/marketplace_service_test.go:77-92Full registration flow with validation
handleReregistrationmarketplace/marketplace_service.go274-469integration/reregistration_test.go:1-200Updates existing app registration
diffPermissionsmarketplace/marketplace_service.go473-501Tested in reregistration flowPermission set difference calculator
InstallAppmarketplace/marketplace_service.go504-645integration/marketplace_service_test.go:95-107Installation with dependency checking
UninstallAppmarketplace/marketplace_service.go648-776integration/marketplace_service_test.go:110-123Uninstallation with impact analysis
DeregisterAppmarketplace/marketplace_service.go809-878integration/marketplace_service_test.go:126-145Complete app removal
configureRateLimitsmarketplace/marketplace_service.go881-931Unit tested in service testsRate limit configuration
RestoreRoutesFromDBmarketplace/marketplace_service.go935-980integration/marketplace_service_test.go:230-260Startup route restoration
RegisterAppCommandmarketplace/commands.go11-19Used throughout testsRegistration command DTO
RegisterAppResponsemarketplace/responses.go9-14Verified in all register testsRegistration response DTO
HasDependentsErrormarketplace/errors.go37-45integration/marketplace_service_test.go:155-170Dependent check error
Route Registry (in-mem)domain/route/registry_inmem.go11-154integration/marketplace_service_test.goThread-safe in-memory registry
Dependency Resolverdomain/service/dependency_resolver.go1-400integration/dependency_resolver_test.goCycle detection, preconditions
Route Conflict Detectordomain/service/route_conflict_detector.go1-300integration/route_conflict_test.goPattern conflict detection
TupleManager (OpenFGA)infrastructure/authz/tuple_manager.go1-250integration/marketplace_authorization_e2e_test.go:46-200Permission tuple management
Settings Integrationmarketplace/marketplace_service.go190-208, 422-452integration/marketplace_settings_registration_test.go:1-150Settings registration/deletion