Skip to main content

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 requests
  • upstream_base_url: Internal backend URL where requests are proxied
  • strip_base_path: If true, removes base_path before forwarding to upstream
  • forward_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 routing
  • default_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.

Implementation Status

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 values
  • int, int! - Integer numbers
  • float, float! - Decimal numbers
  • bool, bool! - Boolean flags
  • string[], int[], float[], bool[] - Arrays
  • json, json! - Arbitrary JSON
  • KVPair[] - 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

  1. Identity Proof: The certificate's Common Name (CN) must match the app name
  2. Request Signing: Used to sign gRPC requests for authentication
  3. 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

Further Reading