Application Manifest & Registration
The application manifest is the core descriptor that defines everything about your application—from its identity and assets to its permissions and dependencies.
Overview
Every application in the Easy AppServer ecosystem is defined by its manifest, which serves as both a declaration of capabilities and a contract with the platform. The manifest undergoes validation, signing, and registration before an app can be installed or run.
Manifest Structure
The manifest follows the protocol buffer schema defined in easy.proto/v2/protos/manifest.proto and contains several key sections:
Identity & Metadata
- name: Unique application identifier (e.g., "de.easy-m.todos")
- certificate: X.509 certificate for authentication
Assets
Assets represent static files that are part of your application:
message Asset {
string name = 1; // e.g., "app.esm.js", "styles.css"
string mime_type = 2; // e.g., "application/javascript"
bytes contents = 3; // Raw file contents
bytes signature = 4; // RSA signature over contents
string sha256 = 5; // SHA-256 hash (hex encoded)
}
Key Points:
- Assets are stored in the database and served with SRI (Subresource Integrity) headers
- SHA-256 hashes and RSA signatures are automatically calculated during asset collection
- Assets are immutable—any change requires re-registration
Code Reference: pkg/v2/application/marketplace/marketplace_service.go:99
UI Configuration
Defines how the application's frontend should be integrated:
WebApp {
integration_mode: MODULE_FEDERATION | WEB_COMPONENT | ESM
entry_point: Main asset file
navigation: Menu items and routes
config: Framework-specific configuration
}
Integration Modes:
- Module Federation: Webpack Module Federation for React/Vue/Angular
- Web Components: Custom elements for framework-agnostic integration
- ESM: ES modules for lightweight apps
Code Reference: pkg/v2/domain/app/app.go:99
API Configuration
Declares how requests to your app should be proxied from the host to your backend:
message WebApi {
string base_path = 1; // e.g., "/api/apps/todos"
string upstream_base_url = 2; // e.g., "http://todos-bff.svc:8080"
bool strip_base_path = 3; // Remove base_path before forwarding
repeated string forward_headers = 5;
repeated RouteSpec routes = 6;
repeated string required_permissions = 7; // API-wide scopes
RateLimit default_rate_limit = 8;
HealthCheck health_check = 9;
}
Key Fields:
base_path: Public-facing path prefix where the host receives requestsupstream_base_url: Internal backend URL where requests are proxiedstrip_base_path: If true, removesbase_pathbefore forwarding to upstreamforward_headers: Allowlist of HTTP headers to forward (e.g.,["authorization", "content-type"])routes: Fine-grained route specifications (optional, see below)required_permissions: API-wide permission scopes enforced before routingdefault_rate_limit: Default rate limit for all routes (can be overridden per-route)health_check: Health check configuration for the upstream service
Code Reference: easy.proto/v2/protos/api.proto:11, pkg/v2/application/proxy/
SDK API Configuration
The SDK supports two patterns for configuring the API:
Builder Pattern (recommended):
.api(api => api
.basePath('/api/apps/todos')
.upstream('http://localhost:3000')
.stripBasePath(true)
.forwardHeaders(['authorization', 'content-type'])
.requiredPermissions(['todos:access'])
.rateLimit({ rpm: 100, burst: 20 })
.healthCheck({ path: '/health' })
)
Object Pattern:
.api({
basePath: '/api/apps/todos',
upstreamBaseURL: 'http://localhost:3000',
stripBasePath: true,
forwardHeaders: ['authorization', 'content-type'],
requiredPermissions: ['todos:access'],
defaultRateLimit: { rpm: 100, burst: 20 },
healthCheck: { path: '/health' }
})
Code Reference: web/v2/packages/appserver-sdk/src/manifest/builder.ts:293
Route Specifications
Fine-grained routing rules can be defined using RouteSpec:
message RouteSpec {
string pattern = 1; // e.g., "/items/**" or "regex:^/items/\\d+$"
repeated string methods = 2; // e.g., ["GET", "POST"]
repeated string scopes = 3; // Additional route-level permissions
int32 timeout_ms = 4; // Override API timeout
RateLimit rate_limit = 5; // Override default rate limit
bool is_public = 6; // If true, skip authentication
}
Code Reference: easy.proto/v2/protos/common.proto:16
Permissions
Declares what permissions the app requires from the platform:
message Permission {
string scope = 1; // e.g., "read_campaigns_any"
string resource_type = 2; // e.g., "campaigns"
string reason = 3; // Human-readable justification
bool requires_approval = 4; // Needs explicit admin approval
}
Example:
required_permissions: [
{
scope: "read_users_any"
resource_type: "users"
reason: "App needs to display user information in dashboards"
requires_approval: false
},
{
scope: "write_campaigns_own"
resource_type: "campaigns"
reason: "Allow users to create their own campaigns"
requires_approval: true
}
]
Permissions are translated into OpenFGA tuples during installation. See Permission Model for details.
Code Reference: easy.proto/v2/protos/manifest.proto:66
SDK Permission APIs
The SDK provides three ways to add required permissions:
1. Single Permission (.requirePermission()):
defineManifest()
.requirePermission('read_campaigns_any', {
resourceType: 'campaigns',
reason: 'Read campaign data for analytics',
requiresApproval: false
})
2. Builder Pattern (.requiredPermissions()):
defineManifest()
.requiredPermissions(perms => perms
.add('read_campaigns_any', { reason: 'Read campaign data' })
.add('write_campaigns_own', {
reason: 'Create campaigns',
requiresApproval: true
})
)
3. Array of Scopes (.requiredPermissions()):
defineManifest()
.requiredPermissions(['read_campaigns_any', 'write_campaigns_own'])
Code Reference: web/v2/packages/appserver-sdk/src/manifest/builder.ts:488
API-Level vs Route-Level Permissions
The platform enforces permissions at two levels:
1. API-Level Permissions (WebApi.required_permissions):
- Applied to the entire API before any routing
- Checked once when a request arrives at
base_path - Must be satisfied for any route under the API to be accessible
2. Route-Level Permissions (RouteSpec.scopes):
- Additional permissions required for specific routes
- Checked after API-level permissions
- Additive: route must satisfy both API-level AND route-level permissions
Example:
defineManifest()
.api(api => api
.basePath('/api/apps/todos')
.upstream('http://localhost:3000')
.requiredPermissions(['todos:access']) // Required for all routes
)
.route('GET', '/items', {
handler: async (request, reply, ctx) => {
// Fetch items logic
return { items: [] };
},
permissions: ['todos:read'] // Additional permission for this route
// Effective permissions: ['todos:access', 'todos:read']
});
Code Reference: easy.proto/v2/protos/api.proto:36, easy.proto/v2/protos/common.proto:25
Public Routes
Routes can be made publicly accessible without authentication:
message RouteSpec {
bool is_public = 6; // If true, skip authentication
}
When is_public is true, the route bypasses all permission checks and allows unauthenticated access. The SDK automatically sets is_public to true when no scopes are provided for a route.
Example:
// Explicit public route
.route('GET', '/health', {
handler: async () => ({ status: 'healthy' }),
isPublic: true
})
// Implicit public route (no permissions)
.route('GET', '/status', {
handler: async () => ({ status: 'ok' })
})
// SDK automatically sets isPublic: true when no permissions specified
Code Reference: easy.proto/v2/protos/common.proto:36, web/v2/packages/appserver-sdk/src/manifest/builder.ts:414
Rate Limiting
Rate limits can be configured at both API and route levels:
API-Level Default (WebApi.default_rate_limit):
message RateLimit {
int32 rpm = 1; // Requests per minute
int32 burst = 2; // Burst capacity
}
Per-Route Override (RouteSpec.rate_limit):
Routes can override the default rate limit with their own configuration.
Example:
defineManifest()
.api(api => api
.basePath('/api/apps/todos')
.upstream('http://localhost:3000')
.rateLimit({ rpm: 100, burst: 20 }) // Default for all routes
)
.route('POST', '/items', {
handler: async (request, reply, ctx) => {
// Create item logic
return { id: '123' };
},
rateLimit: { rpm: 10, burst: 2 } // Stricter limit for writes
});
Code Reference: easy.proto/v2/protos/api.proto:40, easy.proto/v2/protos/common.proto:7
Health Checks
Health checks monitor the upstream service availability:
message HealthCheck {
string path = 1; // e.g., "/health"
int32 interval_seconds = 2; // Default: 30
int32 timeout_ms = 3; // Default: 5000
int32 unhealthy_threshold = 4; // Default: 3
}
Example:
defineManifest()
.api(api => api
.basePath('/api/apps/todos')
.upstream('http://localhost:3000')
.healthCheck({
path: '/health',
intervalSeconds: 30,
timeoutMs: 5000,
unhealthyThreshold: 3
})
);
The platform probes the health endpoint at regular intervals. After consecutive failures exceeding unhealthy_threshold, the service is marked unhealthy for monitoring purposes.
Health check probes are currently advisory only. The marketplace service registers HTTP probes (pkg/v2/application/marketplace/marketplace_service.go:610-625) and monitors health status, but does not currently gate traffic or remove routes when probes fail. Traffic continues to flow to unhealthy services until route gating is implemented.
Code Reference: easy.proto/v2/protos/common.proto:41, web/v2/packages/appserver-sdk/src/manifest/types.ts:188
Dependencies
Specifies relationships with other applications:
dependencies: [
{
name: "de.easy-m.auth"
version: "^2.0.0"
type: HARD // or SOFT
}
]
- HARD dependencies: Must be installed for the app to function
- SOFT dependencies: Optional, app degrades gracefully if missing
See Dependency Management for resolution logic.
Settings Schema
Defines configuration that users can customize:
message SettingsSchema {
repeated SettingDefinition definitions = 1;
}
message SettingDefinition {
string key = 1; // e.g., "api_key", "max_retries"
SettingDataType data_type = 2; // STRING, NUMBER, BOOLEAN, JSON
bool required = 3;
string default_value = 4; // JSON-encoded
ValidationRules validation = 5;
string description = 6;
bool sensitive = 7; // Encrypted at rest if true
UIHints ui_hints = 8;
}
Example:
settings: {
definitions: [
{
key: "api_key"
data_type: STRING
required: true
description: "External API authentication key"
sensitive: true
},
{
key: "max_items"
data_type: NUMBER
required: false
default_value: "100"
description: "Maximum items to display per page"
validation: {
min: 1
max: 1000
}
}
]
}
Settings are validated against their definitions and stored with AES-256-GCM encryption for sensitive values. See Settings Management.
Code Reference: easy.proto/v2/protos/manifest.proto:99
SDK Setting API
The SDK provides a fluent API for defining settings with full TypeScript support:
defineManifest()
.setting({
key: 'api_key',
type: 'string!', // ! suffix means required
label: 'API Key', // Display label for UI
description: 'External API authentication key',
sensitive: true, // Encrypted at rest
uiHints: {
inputType: 'password',
helpText: 'Enter your API key from the external service'
}
})
.setting({
key: 'max_items',
type: 'int',
label: 'Items Per Page',
description: 'Maximum items to display per page',
default: 10,
validation: {
min: 5,
max: 50
},
uiHints: {
inputType: 'number',
step: 5,
helpText: 'Choose how many items to show (5-50)'
}
})
.setting({
key: 'theme',
type: 'string!',
label: 'UI Theme',
description: 'Color theme for the application',
default: 'light',
validation: {
enum: ['light', 'dark', 'auto']
},
uiHints: {
inputType: 'select',
options: ['light', 'dark', 'auto']
}
})
Supported Data Types:
string,string!- Text valuesint,int!- Integer numbersfloat,float!- Decimal numbersbool,bool!- Boolean flagsstring[],int[],float[],bool[]- Arraysjson,json!- Arbitrary JSONKVPair[]- Key-value pairs
Code Reference: web/v2/packages/appserver-sdk/src/manifest/types.ts:259
Certificate Requirements
Every application must provide an X.509 certificate for identity and authentication:
Certificate Purpose
- Identity Proof: The certificate's Common Name (CN) must match the app name
- Request Signing: Used to sign gRPC requests for authentication
- Integrity: Ensures the manifest hasn't been tampered with
Certificate Validation
During registration, the platform validates:
- Certificate format (PEM-encoded X.509)
- Common Name matches manifest name
- Certificate is not expired
- Signature is valid
Code Reference: pkg/v2/domain/app/certificate.go
Bootstrap Registration
The first registration is special—it establishes the app's identity:
- No prior authentication required
- Certificate becomes the app's permanent identity
- Subsequent registrations must be signed with this certificate
Code Reference: pkg/v2/presentation/grpc/grpcauth/authenticator.go:85
Registration Flow
The registration process involves multiple steps and validations:
1. Manifest Submission
Client calls Marketplace.RegisterApp via gRPC with the complete manifest.
Code Reference: easy.proto/v2/protos/services.proto:161
2. Proto to Command Conversion
The gRPC layer converts protocol buffer messages to application commands:
func ConvertManifestToCommand(manifest *pbmanifest.AppManifest) (*marketplace.RegisterAppCommand, error)
Code Reference: pkg/v2/presentation/grpc/converters.go:19
3. Validation Pipeline
The marketplace service validates:
- Manifest structure completeness
- Certificate validity and name matching
- Version format (semantic versioning)
- Dependency references
- Settings schema validity
Code Reference: pkg/v2/application/marketplace/marketplace_service.go:99
4. Asset Persistence
Assets are stored in PostgreSQL:
- Contents stored as bytea
- Checksums calculated and verified
- Associated with the app version
5. Settings Registration
If a settings schema is defined:
- Schema is validated
- Registered with the settings service
- Default values are applied
Code Reference: pkg/v2/application/settings/settings_service.go:30
6. Event Publication
On successful registration, lifecycle events are published to the event bus:
- app.registered
- settings.registered (if applicable)
Code Reference: pkg/v2/domain/event/bus.go:10
7. State Transition
The app transitions to the Registered state, ready for installation.
Code Reference: pkg/v2/domain/app/state.go:4
Node.js SDK Manifest Builder
The @easy/appserver-sdk provides a fluent API for building manifests:
Builder API
import { defineManifest } from '@easy/appserver-sdk';
const manifest = defineManifest()
.name('de.easy-m.todos')
.certificate(certificatePEM)
.ui(ui => ui
.mode('FEDERATION')
.entryAsset('remoteEntry.js')
.routeBase('/apps/todos')
)
.navigation({
path: '/todos',
label: 'Todos',
icon: 'list-icon'
})
// API configuration with builder pattern
.api(api => api
.basePath('/api/apps/todos')
.upstream('http://localhost:3000')
)
// Add routes with inline handlers
.route('GET', '/todos', {
handler: async (request, reply, ctx) => {
ctx.logger.info('Fetching todos', { userId: ctx.user?.id });
return { todos: [] };
},
permissions: ['todos:read']
})
.route('POST', '/todos', {
handler: async (request, reply, ctx) => {
const body = request.body;
// Create todo logic
return { id: '123', ...body };
},
permissions: ['todos:write'],
rateLimit: { rpm: 10, burst: 2 }
})
// Required permissions (builder pattern)
.requiredPermissions(perms => perms
.add('read_users_any', {
reason: 'Display user information in todo assignments',
resourceType: 'users'
})
)
.dependency({ appName: 'de.easy-m.auth', required: true, minVersion: '^2.0.0' })
.setting({
key: 'api_key',
type: 'string!',
label: 'API Key',
description: 'External API authentication key',
sensitive: true,
uiHints: {
inputType: 'password',
helpText: 'Enter your external API key'
}
})
.build();
Automatic Features
The SDK automatically handles:
- Asset Collection: Scans build output for frontend assets
- Checksum Calculation: Computes SHA-256 hashes
- Signature Generation: Signs the manifest with the app certificate
- Validation: Ensures manifest structure is correct before sending
Code Reference: web/v2/packages/appserver-sdk/src/manifest/builder.ts:1
Asset Collection
For Vite projects:
import { collectViteAssets, defineManifest } from '@easy/appserver-sdk';
const builder = defineManifest()
.name('de.easy-m.todos')
.certificate(certificatePEM);
// Collect assets from Vite build
const result = await collectViteAssets({
projectDir: '.',
privateKey: privateKeyPEM,
distDir: 'dist'
});
// Add each asset to the manifest
result.assets.forEach(asset => {
builder.asset(asset);
});
const manifest = builder.build();
The SDK handles:
- Automatic MIME type detection
- Content reading and encoding
- SHA-256 hash and RSA signature generation
Re-Registration
Applications can be re-registered to update their manifest:
Version Changes
- Patch/Minor Updates: Usually accepted automatically
- Major Updates: May require permission review
Manifest Updates
Certain fields can be updated without breaking changes:
- Description and metadata
- Asset contents (with version bump)
- Settings schema (additive changes only)
- New routes/permissions (require re-approval)
Immutable Fields
Some fields cannot be changed after initial registration:
- Application name
- Certificate (use a new app for identity changes)
Validation Rules
The platform enforces strict validation:
Name Format
- Must follow reverse domain notation:
com.company.app - Only lowercase letters, dots, and hyphens
- Must match certificate CN
Asset Constraints
- Individual asset max size: 10 MB
- Total assets per app: 50 MB
- Allowed MIME types:
application/javascript,text/css,image/*,font/*
Route Constraints
- Must start with
/api/ - Cannot conflict with platform routes
- Max 100 routes per app
Security Considerations
Manifest Integrity
- Manifest is signed using the app certificate
- Signature is verified on every gRPC request
- Replay attacks prevented via timestamp validation
Code Reference: pkg/v2/infrastructure/auth/signature.go:25
Asset Integrity
- SRI hashes ensure assets haven't been tampered with
- Browser verifies integrity on load
- Mismatch causes load failure
Permission Principle
- Declare minimum necessary permissions
- Users review permissions before installation
- Platform enforces permissions at runtime
Related Concepts
- Application Lifecycle - What happens after registration
- Settings Management - How settings work
- Developer Platform & SDK - Building manifests with the SDK
- Asset Serving & Microfrontends - How assets are served
- Permission Model - Permission enforcement
- Dependency Management - Dependency resolution
Further Reading
- Getting Started: Building Apps - Practical guide to creating apps
- Protocol Buffers Reference - Complete proto definitions