Skip to main content

Developer Platform & Node SDK

The Easy AppServer SDK provides a comprehensive development environment for building applications with TypeScript support, manifest builders, lifecycle hooks, and client helpers for all platform services.

Overview

The @easy/appserver-sdk package abstracts the complexity of platform integration, providing:

  • Manifest builder with type safety
  • Lifecycle hook orchestration
  • gRPC client generation and helpers
  • GraphQL client utilities
  • Request signing and authentication
  • Development tooling

Package: web/v2/packages/appserver-sdk

SDK Architecture

Core Components

@easy/appserver-sdk/
├─ manifest/ # Manifest builder API
├─ runtime/ # Lifecycle controller & state machine
├─ http/ # HTTP server & request helpers
├─ config/ # Configuration loader
├─ database/ # Database client
└─ utils/ # Logger, certificates, retry logic

Manifest Builder

Code Reference: web/v2/packages/appserver-sdk/src/manifest/builder.ts:1

Fluent API

import { defineManifest } from '@easy/appserver-sdk';

const manifest = defineManifest()
.name('de.easy-m.todos')
.certificate(certificatePEM)

// Web app configuration
.ui(ui => ui
.mode('FEDERATION') // or 'WEB_COMPONENT', 'ESM_COMPONENT'
.entryAsset('remoteEntry.js')
.exposedModule('./App')
.routeBase('/apps/todos')
)

// Navigation
.navigation({
path: '/todos',
label: 'Todos',
icon: 'list-icon'
})
.navigation({
path: '/settings',
label: 'Settings',
icon: 'cog-icon'
})

// API configuration
.api({
basePath: '/api',
upstreamBaseURL: 'http://localhost:3000'
})

// Permissions
.requirePermission('read_users_any')
.requirePermission('trigger_hook_user_created')
.requirePermission('listen_hook_user_updated')
.requirePermission('execute_activity_send_email')

// 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' })

// Settings
.setting({
key: 'api_key',
type: 'string!',
sensitive: true,
validation: {
pattern: '^sk_[a-z]+_[A-Za-z0-9]{32}$'
}
})
.setting({
key: 'max_items',
type: 'int',
default: 100,
validation: { min: 1, max: 1000 }
})

.build();

Asset Collection

Automatically collect and bundle assets:

import { collectViteAssets, defineManifest } from '@easy/appserver-sdk';

const builder = defineManifest()
.name('de.easy-m.todos')
.certificate(certificatePEM);

// Collect from Vite build output
const result = await collectViteAssets({
projectDir: '.',
privateKey: privateKeyPEM,
distDir: 'dist',
skipBuild: false
});

// Add assets to manifest
result.assets.forEach(asset => {
builder.asset(asset);
});

const manifest = builder.build();

Features:

  • Automatic MIME type detection
  • SHA-256 checksum calculation
  • Base64 encoding for binary assets
  • Size validation

Lifecycle Controller

Code Reference: web/v2/packages/appserver-sdk/src/runtime/lifecycle.ts:29

Hook Execution

The SDK orchestrates lifecycle hooks in the correct order:

import { createApp } from '@easy/appserver-sdk';

await createApp({
app: {
// Lifecycle hooks
async onCreate(ctx) {
console.log('App created - initialize resources');
await initializeDatabase();
},

async onRegister(ctx) {
console.log('App registered - setup complete');
},

async onInstall(ctx) {
console.log('App installed - perform setup');
await seedData();
await registerScheduledJobs();
},

async onUninstall(ctx) {
console.log('App uninstalling - cleanup');
await cleanupData();
await unregisterJobs();
},

async onDestroy(ctx) {
console.log('App destroyed - final teardown');
await closeConnections();
}
},
appServerConfig: {
identity: {
name: 'de.easy-m.todos',
privateKeyPath: './certs/key.pem'
},
graphql: {
httpEndpoint: 'http://localhost:8080/graphql',
wsEndpoint: 'ws://localhost:8080/graphql'
}
}
});

Hook Order

Lifecycle Flow:
1. onCreate() - App initialization (once)
2. onRegister() - Registration with platform
3. onInstall() - Installation setup (per instance)

[App Running]

4. onUninstall() - Cleanup before removal
5. onDestroy() - Final teardown (once)

State Synchronization

Code Reference: web/v2/packages/appserver-sdk/src/runtime/state-machine.ts:113

The SDK mirrors the platform's state machine and automatically syncs state transitions. State changes trigger the corresponding lifecycle hooks (onCreate, onRegister, onInstall, etc.) automatically based on events from the AppServer.

Hook Idempotency

Hooks should be idempotent (safe to run multiple times):

let installed = false;

async function onInstall() {
if (installed) {
console.log('Already installed, skipping');
return;
}

await performSetup();
installed = true;
}

Working with AppServer Services

gRPC Clients

Generated TypeScript clients are available in the @easy/client-ts package (separate from the SDK):

import { createMarketplaceServiceClient } from '@easy/client-ts';

// Create client with credentials
const marketplace = createMarketplaceServiceClient({
address: 'localhost:50051',
appName: 'de.easy-m.todos',
privateKeyPath: './certs/key.pem'
});

// Register app
await marketplace.registerApp({ manifest });

// List installed apps
const apps = await marketplace.listApps({});

For full gRPC service documentation, see gRPC Services.

GraphQL Queries

Use standard GraphQL clients like Apollo or urql. The SDK provides authenticated appFetch for requests:

import { appFetch } from '@easy/appserver-sdk';

// Query with appFetch
const response = await appFetch('http://localhost:8080/graphql', {
method: 'POST',
appName: 'de.easy-m.todos',
privateKeyPath: './certs/key.pem',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: `
query GetApps {
apps {
name
state
}
}
`
})
});

const { data } = await response.json();

For GraphQL API documentation, see GraphQL API & Subscriptions.

Authentication Helpers

Certificate Management

import { loadCertificate, loadPrivateKey } from '@easy/appserver-sdk';

// Load certificate and private key
const cert = await loadCertificate('./cert.pem');
const key = await loadPrivateKey('./key.pem');

Certificates should be generated using standard tools like OpenSSL. See Authentication & Authorization for certificate generation instructions.

Request Signing

Code Reference: web/v2/packages/appserver-sdk/src/http/app-fetch.ts:34-128

The SDK provides appFetch for making authenticated requests to other apps:

import { appFetch } from '@easy/appserver-sdk';

// Authenticated inter-app request
const response = await appFetch('http://another-app/api/data', {
method: 'POST',
body: JSON.stringify({ key: 'value' }),
appName: 'de.easy-m.todos',
privateKeyPath: './certs/key.pem',
headers: {
'Content-Type': 'application/json'
}
});

// Behind the scenes:
// 1. Create signature payload: METHOD\n/path?query\nTIMESTAMP\nAPPNAME
// 2. Sign with RSA-SHA256 private key
// 3. Add metadata headers:
// X-App-Name: de.easy-m.todos
// X-App-Timestamp: 2025-01-24T15:30:00Z (RFC3339 format)
// X-App-Signature: base64-encoded-rsa-signature

The signature format matches AppServer's authenticator expectations in pkg/v2/presentation/grpc/grpcauth/authenticator.go:89-155.

Working with Hooks and Activities

Hooks and activities are managed through gRPC services. Use the @easy/client-ts package to interact with them:

import { createHooksServiceClient } from '@easy/client-ts';

const hooksClient = createHooksServiceClient({
address: 'localhost:50051',
appName: 'de.easy-m.todos',
privateKeyPath: './certs/key.pem'
});

// Trigger a hook
await hooksClient.trigger({
hookName: 'user.created',
payload: { userId: '123', email: 'user@example.com' }
});

For detailed information on hooks and activities, see:

Settings Management

Settings are defined in the manifest and accessed through GraphQL or gRPC:

// Define in manifest
.setting({
key: 'api_key',
type: 'string!',
sensitive: true
})
.setting({
key: 'max_items',
type: 'int',
default: 100
})

Access settings at runtime via GraphQL or the settings service client from @easy/client-ts.

For detailed information, see Settings Management.

Development Tooling

Local Development

# Watch and rebuild SDK on changes
npm run dev
Implementation Status

The npm run dev script runs tsup --watch (web/v2/packages/appserver-sdk/package.json:15-24), which only watches and rebuilds the SDK itself. It does not:

  • Auto-connect your application to the platform
  • Reload manifests automatically
  • Start your application process

To develop an application:

  1. Run npm run dev in the SDK package (optional, for SDK development)
  2. Start your application process separately using createApp() from the SDK
  3. Your application connects to the platform when it calls lifecycle hooks

The SDK provides building blocks; you must implement your own development server and hot-reload logic for your application.

TypeScript Support

Full type safety with SDK types:

import type { AppContext, SDKOptions } from '@easy/appserver-sdk';

// Context is typed with your config
interface MyConfig {
apiKey: string;
maxRetries: number;
}

const app: SDKOptions<MyConfig> = {
app: {
async onCreate(ctx: AppContext<MyConfig>) {
// ctx.config is typed as MyConfig
console.log(ctx.config.apiKey);
}
},
appServerConfig: {
// ...
}
};

Best Practices

Error Handling

async function onInstall() {
try {
await setupDatabase();
} catch (err) {
console.error('Setup failed:', err);
throw err; // Platform marks installation as failed
}
}

Graceful Shutdown

// The SDK handles graceful shutdown automatically
// SIGTERM/SIGINT signals trigger the onDestroy hook
process.on('SIGTERM', () => {
console.log('Received SIGTERM, SDK will handle cleanup');
// SDK automatically calls onDestroy() before exit
});

Logging

import { createLogger } from '@easy/appserver-sdk';

const logger = createLogger({
appName: 'de.easy-m.todos',
level: 'info'
});

logger.info('Processing request', { userId: '123' });
logger.error('Failed to save', { error: err });

Testing

Test your app by running it locally against a development AppServer instance. Use standard testing frameworks like Jest or Vitest:

import { describe, it, expect } from 'vitest';

describe('App lifecycle', () => {
it('should initialize database onCreate', async () => {
// Test your lifecycle hooks with mocked dependencies
const mockDb = createMockDatabase();
await onCreate({ db: mockDb });
expect(mockDb.connected).toBe(true);
});
});

Further Reading