Application Lifecycle
Easy AppServer orchestrates applications through a comprehensive lifecycle, managing registration, installation, running state, and removal with built-in dependency management, event emission, and state validation.
Overview
Applications interact with the Marketplace service to register their capabilities, install when dependencies are satisfied, run with health monitoring, and clean up resources when uninstalled. The platform provides bidirectional streaming for lifecycle notifications and state synchronization.
Code Reference: pkg/v2/application/marketplace/marketplace_service.go:99
Lifecycle States
Applications progress through four persisted states with validated transitions:
StateUnregistered → StateRegistered → StateInstalled → StateUninstalled
↑ ↓
└────────────── re-registration ──────┘
The Go state machine only knows about the states above; “Installing”, “Running”,
“Unhealthy”, or “Failed” are SDK-level concepts and are not persisted by the
server. pkg/v2/domain/app/state.go and the lifecycle validators prevent any
other transition ordering.
Code Reference: pkg/v2/domain/app/state.go:4
Registration Flow
Initial Registration
Code Reference: pkg/v2/application/marketplace/marketplace_service.go:99
RegisterApp Command:
├─ 1. Create app entity with certificate
├─ 2. Create aggregate for domain validation
├─ 3. Execute domain logic (validate certificate, assets, navigation)
├─ 4. Check for existing app (detect re-registration)
├─ 5. Validate dependencies (existence + cycle detection; version constraints are stored but not enforced yet)
├─ 6. Persist app to database
├─ 7. Persist dependencies
├─ 8. Store assets with SHA-256 checksums
├─ 9. Register settings schema (if present)
├─ 10. Create permission tuples in OpenFGA
├─ 11. Register routes in registry and database
└─ 12. Publish app.registered event
Re-Registration
When an app with the same name already exists, re-registration updates its configuration:
Code Reference: pkg/v2/application/marketplace/marketplace_service.go:272
Re-Registration Flow:
├─ 1. Calculate permission changes (diff old vs new)
├─ 2. Sync permissions in OpenFGA (revoke old, grant new)
├─ 3. Update routes (delete old, register new)
├─ 4. Update app certificate and WebApp config
├─ 5. Transition StateUninstalled → StateRegistered (if uninstalled)
├─ 6. Delete and recreate dependencies
├─ 7. Delete and store updated assets
├─ 8. Delete and re-register settings schema
└─ 9. Publish app.registered event
Re-registration allows apps to update their manifests without losing their app ID.
Certificate Validation
Apps must provide a valid X.509 certificate:
Certificate Requirements:
├─ Valid signature chain
├─ Not expired
├─ Common Name (CN) matches app name
└─ Used for request signing
Code Reference: pkg/v2/infrastructure/auth/signature.go:25
Installation Flow
Install Command
Code Reference: pkg/v2/application/marketplace/marketplace_service.go:504
InstallApp Command:
├─ 1. Retrieve app by ID or name
├─ 2. Validate dependencies are satisfied
├─ 3. Validate required settings are configured
├─ 4. Check for route conflicts with existing apps
├─ 5. Create aggregate and execute Install domain logic
├─ 6. Update app state to StateInstalled
├─ 7. Refresh route configurations
├─ 8. Publish app.installed event
└─ 9. Event subscribers (Docker orchestrator, Marketplace.Subscribe stream) react asynchronously
Installation Preconditions
// Dependency validation
if err := s.dependencyResolver.CheckInstallPreconditions(ctx, appID); err != nil {
return nil, fmt.Errorf("dependency validation failed: %w", err)
}
// Settings validation
validationResp, err := s.settingsService.ValidateRequiredSettings(ctx, validateQuery)
if !validationResp.Valid && len(validationResp.MissingKeys) > 0 {
return nil, fmt.Errorf("required settings missing: %v", validationResp.MissingKeys)
}
Installation fails if:
- HARD dependencies are not installed
- Required settings lack values
- Route conflicts exist
Docker Orchestration
If Docker orchestration is enabled, the dedicated InstallationOrchestrator
subscribes to app.installed events and provisions containers out-of-band. The
marketplace service itself only updates state and publishes the event.
Code Reference: pkg/v2/application/orchestration/installer.go
Docker Deployment:
├─ Create deployment record
├─ Pull Docker image
├─ Create container with environment variables
├─ Configure health checks
├─ Start container
├─ Monitor health
└─ Auto-restart on failure
Uninstallation Flow
Uninstall Command
Code Reference: pkg/v2/application/marketplace/marketplace_service.go:648
UninstallApp Command:
├─ 1. Retrieve app by ID or name
├─ 2. Check for dependents (fail if HARD dependents exist, unless force=true)
├─ 3. Create aggregate and execute Uninstall domain logic
├─ 4. Update app state to StateUninstalled
├─ 5. Deregister routes from registry and database
├─ 6. Publish app.uninstalled event (triggers cache invalidation + orchestrator cleanup)
└─ 7. Event subscribers (Docker orchestrator, Marketplace.Subscribe stream) react asynchronously
Dependent Checks
// Check for dependents
dependentIDs, hasHard, err := s.dependencyResolver.CheckUninstallImpact(ctx, appID)
if hasHard && len(dependentIDs) > 0 {
return nil, &HasDependentsError{
AppName: foundApp.Name,
Dependents: dependentNames,
}
}
Uninstallation is blocked if:
- Other apps have HARD dependencies on this app
- Unless
force: trueflag is set (dangerous)
Lifecycle Streaming
Subscribe Protocol
Apps connect to the Marketplace service via bidirectional streaming to receive lifecycle notifications.
Code Reference: pkg/v2/presentation/grpc/marketplace_server.go:79
Subscribe Stream:
├─ App → Server: AppRegisterRequest (manifest)
├─ Server: Validate authentication (app certificate)
├─ Server: Register app and persist manifest
├─ Server → App: StreamingAppResponse (registered confirmation)
│
├─ Server: Subscribe to event bus for lifecycle events
│ ├─ EventTypeAppRegistered
│ ├─ EventTypeAppInstalled
│ ├─ EventTypeAppUninstalled
│ └─ EventTypeAppDeregistered
│
├─ [Event occurs]: app.installed
├─ Server → App: StreamingAppResponse (INSTALL notification)
├─ App: Execute onInstall() lifecycle hook
├─ App: Respond with success/failure
│
└─ Stream remains open for future lifecycle events
Code Reference: easy.proto/v2/protos/services.proto:205
StreamingAppResponse
message StreamingAppResponse {
string id = 1;
oneof message {
AppStateResponse app_state_response = 2;
google.rpc.Status error = 3;
}
}
message AppStateResponse {
bool registered = 1;
bool installed = 2;
bool uninstalled = 3;
}
Event-Based Notifications
The Subscribe stream bridges the event bus to app streams:
// Event handler with strict app isolation
handler := func(eventCtx context.Context, evt event.Event) error {
// Extract app name from event
appName, err := extractAppNameFromEvent(evt)
// SECURITY: Each app only receives events about itself
if appName != authenticatedAppName {
return nil // Skip event
}
// Send lifecycle notification to app
select {
case eventChan <- convertToStreamingResponse(evt):
case <-ctx.Done():
return ctx.Err()
}
return nil
}
// Subscribe to all lifecycle event types
for _, eventType := range eventTypes {
s.eventBus.Subscribe(eventType, handler)
}
Apps only receive notifications about their own lifecycle events for security isolation.
Lifecycle Hooks
Apps implement lifecycle hooks to respond to state changes. The SDK orchestrates hook execution.
Code Reference: web/v2/packages/appserver-sdk/src/runtime/lifecycle.ts:29
Hook Execution Order
Application Startup:
1. onCreate() - Process initialization (once)
2. onStartUp() - After platform connection
3. onRegister() - After manifest registration
↓
[Platform sends INSTALL notification]
↓
4. onInstall() - Installation setup
↓
[App Running]
↓
[Platform sends UNINSTALL notification]
↓
5. onUninstall() - Cleanup before removal
6. onDestroy() - Final teardown (once)
onCreate Hook
Called when the application process starts, before connecting to AppServer:
async onCreate() {
console.log('App created - initialize resources');
await initializeDatabase();
await loadConfiguration();
}
Use Cases:
- Database connection setup
- Configuration loading
- Resource initialization
onStartUp Hook
Called after successful connection to AppServer:
async onStartUp() {
console.log('Connected to AppServer');
await verifyPlatformConnection();
}
onRegister Hook
Called when the app registers its manifest with the platform:
async onRegister() {
console.log('App registered - manifest accepted');
await setupEventSubscriptions();
}
Use Cases:
- Event bus subscription setup
- Hook registration
- Activity handler registration
onInstall Hook
Called when the app is being installed (receives INSTALL notification):
async onInstall() {
console.log('App installing - perform setup');
// Database migrations
await runMigrations();
// Seed initial data
await seedDefaultData();
// Register scheduled jobs
await registerScheduledJobs();
}
Use Cases:
- Database schema migrations
- Initial data seeding
- Scheduled job registration
- Third-party service setup
Important: Hook must be idempotent (safe to run multiple times).
onUninstall Hook
Called when the app is being uninstalled (receives UNINSTALL notification):
async onUninstall() {
console.log('App uninstalling - cleanup');
// Cleanup data
await cleanupApplicationData();
// Unregister jobs
await unregisterScheduledJobs();
// Notify external services
await notifyExternalServices();
}
Use Cases:
- Data cleanup (user data, cache)
- Unregister scheduled jobs
- Close third-party connections
- Export/backup data
onDestroy Hook
Called when the application process is shutting down:
async onDestroy() {
console.log('App destroyed - final teardown');
await closeConnections();
await flushLogs();
}
Use Cases:
- Close database connections
- Flush logs
- Release resources
Hook Error Handling
If a lifecycle hook throws an error, the platform marks the operation as failed:
async onInstall() {
try {
await performSetup();
} catch (err) {
console.error('Setup failed:', err);
throw err; // Platform marks installation as failed
}
}
Failed installations keep the app in its previous state; the marketplace service returns an error and does not persist an intermediate “failed” state.
State Transitions
Valid state transitions are enforced by the state machine:
Code Reference: pkg/v2/domain/app/state.go
Allowed Transitions:
├─ StateUnregistered → StateRegistered (via RegisterApp)
├─ StateRegistered → StateInstalled (via InstallApp)
├─ StateRegistered → StateUnregistered (deregister)
├─ StateInstalled → StateUninstalled (via UninstallApp)
├─ StateUninstalled → StateRegistered (re-registration)
└─ StateUninstalled → StateUnregistered (full removal)
Invalid transitions are rejected:
func (sm *StateMachine) Transition(ctx context.Context, app *app.App, targetState app.State) error {
if !sm.isValidTransition(app.State, targetState) {
return &InvalidTransitionError{
From: app.State,
To: targetState,
}
}
app.State = targetState
return nil
}
Lifecycle Events
The platform publishes events for lifecycle state changes:
Code Reference: pkg/v2/domain/event/bus.go:10
Event Types
EventTypeAppRegistered - App manifest registered
EventTypeAppInstalled - App installation completed
EventTypeAppUninstalled - App uninstallation completed
EventTypeAppDeregistered - App removed from marketplace
Event Payload
type AppRegisteredEvent struct {
AppID uuid.UUID
AppName string
Timestamp time.Time
}
Event Subscribers
Other apps and platform services can subscribe to lifecycle events:
eventBus.subscribe('app.installed', async (event) => {
console.log('App installed:', event.appName);
await updateDashboard(event.appID);
});
Dependency Impact
Installation Dependencies
Apps cannot install until their HARD dependencies are installed:
Code Reference: pkg/v2/domain/service/dependency_resolver.go:25
Installation Dependency Check:
├─ Load all dependencies for app
├─ For each HARD dependency:
│ ├─ Check if dependency app exists
│ └─ Verify dependency app is installed
├─ For each SOFT dependency:
│ ├─ Check if dependency app exists
│ └─ Log warning if missing (don't block)
└─ Return success if all HARD dependencies satisfied
> Version constraints are stored with each dependency but are not evaluated yet.
Uninstallation Impact
Apps with HARD dependents cannot be uninstalled (unless forced):
Uninstall Impact Check:
├─ Find all apps that depend on this app
├─ Filter for HARD dependencies
├─ If HARD dependents exist:
│ └─ Return HasDependentsError with list
└─ Otherwise: Allow uninstall
Installation Order
When installing multiple apps with dependencies, the platform resolves the correct order:
Example:
- App A depends on App B (HARD)
- App B depends on App C (HARD)
Installation Order: C → B → A
Health Monitoring
Container Health Checks
After installation, Docker containers are monitored for health:
Code Reference: pkg/v2/application/orchestration/health_monitor.go:16
Health Monitor Loop (every 30s):
├─ Query healthy deployments
├─ Inspect each container via Docker API
├─ Check container health status:
│ ├─ Healthy: No action
│ ├─ Unhealthy: Mark deployment as unhealthy, attempt restart
│ └─ Exited/Stopped: Restart immediately
├─ Auto-restart unhealthy containers (up to MaxRestarts)
└─ Transition the deployment (not the app) to `deployment.StateFailed` if max restarts are exceeded
Health Endpoints
Apps should expose health endpoints for Docker health checks:
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
Automatic Recovery
The health monitor automatically restarts failed containers:
Container Failure:
├─ 1. Mark deployment as StateUnhealthy
├─ 2. Stop existing container
├─ 3. Remove container
├─ 4. Create new container from same image
├─ 5. Start container
├─ 6. Increment restart count
└─ 7. Publish deployment.recovered event (if successful)
If max restarts are exceeded (default: 3), the deployment record transitions to
deployment.StateFailed and the monitor stops retrying automatically.
Graceful Shutdown
SIGTERM Handling
Apps should handle SIGTERM for graceful shutdown:
process.on('SIGTERM', async () => {
console.log('Shutting down gracefully');
// Stop accepting new requests
await app.stop();
// Close connections
await database.close();
// Flush logs
await logger.flush();
process.exit(0);
});
Connection Draining
Before shutdown, apps should:
- Stop accepting new requests
- Complete in-flight requests
- Close gRPC streams
- Disconnect from event bus
- Close database connections
Shutdown Timeout
Platform waits for graceful shutdown (default: 30s) before forcing termination:
Shutdown Flow:
├─ Send SIGTERM to container
├─ Wait up to 30 seconds
├─ If still running: Send SIGKILL
└─ Remove container
Examples
First-Time Installation
1. Developer creates app manifest
2. App calls RegisterApp with manifest
3. Platform validates and stores manifest
4. Platform publishes app.registered event
5. Operator calls InstallApp
6. Platform validates dependencies and settings
7. Platform publishes app.installed event
8. Optional Docker orchestrator (subscribed to the event) provisions containers
9. Marketplace.Subscribe stream pushes the updated AppStateResponse
10. App executes onInstall() hook
11. App is now running
Reinstallation After Uninstall
1. App is in StateUninstalled
2. Operator calls InstallApp
3. Platform skips manifest re-registration (already registered)
4. Platform validates dependencies and settings
5. Platform publishes app.installed event
6. Optional Docker orchestrator provisions containers
7. Marketplace.Subscribe stream pushes the updated AppStateResponse
8. App executes onInstall() hook (should be idempotent)
9. App is running again
Recovery From Failure
1. App installation fails (onInstall() throws error)
2. Marketplace.InstallApp returns an error and the app remains in its previous state
3. Developer fixes the manifest or hook implementation
4. Operator calls InstallApp again
5. Platform retries installation with the updated manifest
6. Installation succeeds and publishes app.installed
7. Subscribers observe the new StateInstalled status
Dependency Chain Installation
Apps:
- app-c (no dependencies)
- app-b (depends on app-c HARD)
- app-a (depends on app-b HARD)
Installation:
1. Register all apps (can be in any order)
2. Install app-c (no dependencies)
3. Try to install app-a → FAILS (app-b not installed)
4. Install app-b (app-c is installed, OK)
5. Install app-a (app-b is installed, OK)
Best Practices
Manifest Design
// Good: Clear, versioned dependencies
.dependency({ appName: 'de.easy-m.auth', required: true, minVersion: '^2.0.0' })
.dependency({ appName: 'de.easy-m.analytics', required: false, minVersion: '^1.0.0' })
// Bad: Overly restrictive
.dependency({ appName: 'de.easy-m.auth', required: true, minVersion: '=2.0.0' }) // Exact version
Idempotent Hooks
// Good: Check state before performing actions
async onInstall() {
const exists = await database.tableExists('users');
if (!exists) {
await database.createTable('users');
}
}
// Bad: Assume clean state
async onInstall() {
await database.createTable('users'); // Fails if table exists
}
Error Handling
// Good: Specific error messages
async onInstall() {
try {
await runMigrations();
} catch (err) {
throw new Error(`Migration failed: ${err.message}`);
}
}
// Bad: Silent failures
async onInstall() {
try {
await runMigrations();
} catch (err) {
console.log('Migration failed'); // Platform thinks install succeeded
}
}
Health Checks
// Good: Comprehensive health check
app.get('/health', async (req, res) => {
const dbHealthy = await database.ping();
const cacheHealthy = await cache.ping();
if (dbHealthy && cacheHealthy) {
res.status(200).json({ status: 'healthy' });
} else {
res.status(503).json({ status: 'unhealthy' });
}
});
// Bad: Always healthy
app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy' });
});
Troubleshooting
Installation Fails
Problem: InstallApp returns error
Diagnosis:
1. Check dependencies: Are all HARD dependencies installed?
2. Check settings: Are required settings configured?
3. Check logs: Did onInstall() hook throw error?
4. Check routes: Are there route conflicts?
Solutions:
- Install missing dependencies first
- Configure required settings
- Fix onInstall() hook logic
- Change route paths to avoid conflicts
App Never Reaches StateInstalled
Problem: Marketplace never reports StateInstalled
Diagnosis:
1. Check if onInstall() hook is hanging (timeout, deadlock)
2. Check if the `app.installed` event was published (server logs)
3. Check Subscribe stream connection
Solutions:
- Add timeout to onInstall() operations
- Verify Subscribe stream is connected and consuming events
- Check network connectivity
Container Keeps Restarting
Problem: Health monitor keeps restarting container
Diagnosis:
1. Check container logs: Why is health check failing?
2. Check health check configuration: Is timeout too short?
3. Check resource limits: Is container OOM?
Solutions:
- Fix application health check endpoint
- Increase health check timeout
- Increase container memory limits
Related Concepts
- App State Machine & Lifecycle Enforcement - State transition validation
- Dependency Management - Dependency resolution
- Application Manifest & Registration - Manifest structure
- Health Monitoring & Recovery - Health checks and auto-recovery
- Event-Driven Architecture - Lifecycle event bus
- Developer Platform & SDK - SDK lifecycle hooks
Further Reading
- Getting Started: Installation - Installation guide
- Development: Building Apps - App development guide