App Migration Guide: AppServer v1 to v2
This guide covers migrating Easy apps from AppServer v1 SDK to the new AppServer v2 SDK.
1. Overview
1.1 Key Architecture Changes
| Aspect | AppServer v1 | AppServer v2 |
|---|---|---|
| Backend Model | Interface embedding (easysdk.App) | Interface implementation (sdk.App) |
| Backend Configuration | Config files + environment variables | Manifest settings + environment variables |
| Frontend Configuration | app$config DOM events | SDK composables |
| Frontend Integration | Standalone app with event listeners | Module Federation (MFE) |
| HTTP Requests | Direct fetch with auth tokens | SDK fetch through AppServer proxy |
| Asset Delivery | Separate static hosting | Signed assets in manifest |
| Inter-App Communication | Direct HTTP calls | AppServer-mediated proxy with signatures |
1.2 SDK Package Reference
| Purpose | Go SDK | Node.js SDK |
|---|---|---|
| Core SDK | easy.appserver/pkg/v2/sdk | @easy/appserver-sdk |
| Manifest builder | easy.appserver/pkg/v2/sdk/manifest | defineManifest() from SDK |
| Asset collection | sdk/manifest/collectors | collectViteAssets() from SDK |
| Frontend SDK | N/A | @easy/appserver-frontend-sdk |
| Frontend composables | N/A | @easy/appserver-frontend-sdk/vue |
1.3 Configuration Model - CRITICAL
v2 apps use ONLY environment variables and manifest settings. All config files must be REMOVED.
| Configuration Type | v1 Approach | v2 Approach | Migration Action |
|---|---|---|---|
| App settings (feature flags, limits, endpoints) | config/*.yml files | Manifest settings | DELETE config files |
| Frontend configuration | app$config DOM events | useAppSettings() composable | DELETE event listeners |
| Frontend config helpers | use-config.ts files | SDK composables | DELETE these files |
| ConfigGuard components | Wait for app$config | Not needed | DELETE entirely |
| Infrastructure (DB, gRPC address) | Environment variables | Environment variables | Keep as-is |
| Certificates for signing | /config/cert.pem, /config/key.pem | /config/cert.pem, /config/key.pem | Keep as-is |
What you MUST delete when migrating:
- All
config/*.ymlorconfig/*.yamlfiles (app configuration) - All config file loading/parsing code in your backend
- All
app$configevent listeners in your frontend - All
use-config.tsor similar config-reading files - All ConfigGuard wrapper components
What you keep:
/config/cert.pemand/config/key.pem(certificate and private key for manifest signing)- Infrastructure environment variables (
APPSERVER_GRPC_ADDRESS,APP_ENV)
2. Decision Tree: Choose Your Backend SDK
Is your app backend in Go or Node.js?
├── Go → Use easy.appserver/pkg/v2/sdk
│ └── Section 3: Go SDK Migration
│
└── Node.js → Use @easy/appserver-sdk
└── Section 4: Node.js SDK Migration
Does your app have a Vue frontend?
├── Yes → Use @easy/appserver-frontend-sdk
│ └── Section 5: Frontend Migration
│
└── No → Skip Section 5
3. Go SDK Migration
3.1 Import Changes
Before (v1):
import (
easysdk "bitbucket.org/easymarketinggmbh/de.easy-m.sdk-go"
"bitbucket.org/easymarketinggmbh/de.easy-m.sdk-go/app"
)
After (v2):
import (
sdk "bitbucket.org/easymarketinggmbh/easy.appserver/pkg/v2/sdk"
"bitbucket.org/easymarketinggmbh/easy.appserver/pkg/v2/sdk/manifest"
"bitbucket.org/easymarketinggmbh/easy.appserver/pkg/v2/sdk/manifest/collectors"
)
3.2 App Structure Migration
Before (v1) - Interface embedding:
type App struct {
easysdk.App
dbManager *database.Manager // Optional custom fields
}
var _ easysdk.App = &App{}
func NewApp() *App {
return &App{
App: easysdk.NewDefaultApp(),
}
}
func (a *App) OnStartUp() error { ... }
func (a *App) OnCreate() (*app.Manifest, error) { ... }
func (a *App) OnInstall() error { ... }
func (a *App) GetID() string { return "de.easy-m.my-app" }
After (v2) - Interface implementation:
type MyApp struct {
name string
// your fields (no SDK embedding)
}
var _ sdk.App = (*MyApp)(nil)
func (app *MyApp) Name() string { return app.name }
func (app *MyApp) OnStartUp(ctx *sdk.LifecycleContext) error { ... }
func (app *MyApp) OnCreate(ctx *sdk.LifecycleContext) (*manifest.Manifest, error) { ... }
func (app *MyApp) OnRegister(ctx *sdk.LifecycleContext) error { ... }
func (app *MyApp) OnInstall(ctx *sdk.LifecycleContext) error { ... }
func (app *MyApp) OnUninstall(ctx *sdk.LifecycleContext) error { ... }
func (app *MyApp) OnDestroy(ctx *sdk.LifecycleContext, reason string) error { ... }
3.3 Lifecycle Hooks Reference
| Hook | When Called | Purpose |
|---|---|---|
OnStartUp(ctx) | Before network | Early setup (DB available via ctx.DB) |
OnCreate(ctx) | After startup | Build and return manifest |
OnRegister(ctx) | After registration | Post-registration setup |
OnInstall(ctx) | App installed | Service initialization |
OnUninstall(ctx) | App uninstalled | Cleanup resources |
OnDestroy(ctx, reason) | Shutdown | Final cleanup |
Key Change: Database client is now provided via ctx.DB - no manual initialization needed. The SDK manages database connections automatically.
3.4 Manifest Builder Pattern
The manifest is now built in OnCreate() using a fluent builder API:
func (app *MyApp) OnCreate(ctx *sdk.LifecycleContext) (*manifest.Manifest, error) {
// Load certificate
certData, err := os.ReadFile(sdk.ResolvePath(ctx, "config/cert.pem"))
if err != nil {
return nil, fmt.Errorf("failed to load certificate: %w", err)
}
builder := manifest.DefineManifest().
Name(app.name).
Certificate(certData).
PrivateKey(sdk.ResolvePath(ctx, "config/key.pem"))
// Define settings - REPLACES ALL config files and app$config events
// DELETE your config/*.yml files and all config file loading code
// Note: Entity manager endpoint not needed - HTTP proxy handles routing
builder.Setting(&manifest.SettingConfig{
Key: "feature_enabled",
Type: "bool",
Label: "Enable Feature",
Description: "Toggle to enable/disable feature",
Default: true,
})
// Configure UI (for apps with frontend)
builder.UI(func(ui *manifest.UIBuilder) *manifest.UIBuilder {
return ui.
Mode(manifest.UIModeESMComponent).
EntryAsset("remoteEntry.js").
RouteBase("/apps/myapp").
ExposedModule("MyApp")
})
// Configure API routes (HTTP proxy routes to upstream automatically)
builder.API(func(api *manifest.APIBuilder) *manifest.APIBuilder {
return api.
Upstream(sdk.ResolveUpstream(app, "http://localhost:8081")).
ForwardHeaders([]string{"Authorization", "X-User-ID", "X-Tenant-ID"}).
RouteWithConfig("POST", "/query", manifest.HandlerFunc(app.handleGraphQL), nil)
})
// Required permissions
builder.RequiredPermission("read_campaigns_any", "campaigns", "Read campaign data")
return builder.Build()
}
3.5 Asset Collection (Go + Vue Apps)
For apps with Vue frontends, collect Vite-built assets in OnCreate(). Use SDK helpers for path resolution and production detection:
func (app *MyApp) OnCreate(ctx *sdk.LifecycleContext) (*manifest.Manifest, error) {
// ... certificate loading ...
// In production, assets are pre-built during Docker image build
// In development, build them on startup
skipInstallBuild := sdk.IsProduction(ctx)
// Collect frontend assets
result, err := collectors.CollectMakeAssets(&collectors.MakeAssetCollectorOptions{
WorkDir: ".",
PrivateKeyPath: sdk.ResolvePath(ctx, "config/key.pem"), // Resolves to /config/key.pem in production
DistDir: sdk.ResolvePath(ctx, "web/packages/my-app/dist"),
InstallTarget: "web-install",
BuildTarget: "web-build",
SkipInstall: skipInstallBuild, // Skip in production - pre-built in CI/CD
SkipBuild: skipInstallBuild, // Skip in production - pre-built in CI/CD
})
if err != nil {
return nil, fmt.Errorf("failed to collect assets: %w", err)
}
builder := manifest.DefineManifest()
// ... other builder config ...
// Add assets to manifest
for _, asset := range result.Assets {
builder.AssetWithSignature(
asset.Name,
asset.MimeType,
asset.Contents,
asset.Signature,
asset.SHA256,
)
}
return builder.Build()
}
3.6 SDK Helper Functions
The SDK provides helper functions for path resolution and environment detection. See pkg/v2/sdk/helpers.go.
IsProduction() - Environment Detection
// Returns true for production (not "debug" or "development")
if sdk.IsProduction(ctx) {
// Skip web install/build - assets pre-built in CI/CD
}
ResolvePath() - Docker Path Resolution
Converts relative paths to absolute paths in production (for Docker containers where files are at root):
// Development: "config/key.pem" -> "config/key.pem"
// Production: "config/key.pem" -> "/config/key.pem"
certPath := sdk.ResolvePath(ctx, "config/cert.pem")
keyPath := sdk.ResolvePath(ctx, "config/key.pem")
distDir := sdk.ResolvePath(ctx, "web/packages/my-app/dist")
WARNING: The /config/ directory is ONLY for certificate and key files (cert.pem, key.pem) used for manifest signing. Do NOT put application configuration files here. All app configuration must be defined as manifest settings or infrastructure environment variables.
ResolveUpstream() - Upstream URL Resolution
Checks environment variable UPSTREAM_{APP_NAME} for override, or uses default:
// Checks UPSTREAM_DE_EASY_M_DATA_SOURCES env var
// Falls back to "http://localhost:1337" if not set
builder.API(func(api *manifest.APIBuilder) *manifest.APIBuilder {
return api.Upstream(sdk.ResolveUpstream(app, "http://localhost:1337"))
})
// For other apps:
// Checks UPSTREAM_DE_EASY_M_ENTITY_MANAGER
emURL := sdk.ResolveUpstreamFor("de.easy-m.entity-manager", "http://localhost:8081")
AppNameToEnvVar() - Environment Variable Naming
sdk.AppNameToEnvVar("de.easy-m.entity-manager") // "DE_EASY_M_ENTITY_MANAGER"
sdk.AppNameToEnvVar("de.easy-m.entity-manager", "UPSTREAM") // "UPSTREAM_DE_EASY_M_ENTITY_MANAGER"
3.7 Main Entry Point
Before (v1):
var config = flag.String("c", "./config/config.yml", "the app config file")
func main() {
flag.Parse()
app := the_app.NewApp()
sdk := easysdk.NewSDK(context.Background(), app, easysdk.WithConfig(*config))
if err := sdk.Run(); err != nil {
log.Fatalf("running app failed: %v", err)
}
}
After (v2):
func main() {
ctx := context.Background()
app := myapp.NewMyApp()
// Configuration is EXCLUSIVELY via environment variables and manifest settings
// DELETE all config file loading code - config files are NOT supported in v2
opts := &sdk.CreateAppOptions{
MarketplaceEnabled: true,
}
sdk.Run(ctx, app, opts)
}
4. Node.js SDK Migration
For apps that don't need Go-specific features (e.g., frontend-only apps), migrate from Go to Node.js SDK.
4.1 Project Structure
my-nodejs-app/
├── src/
│ └── index.ts # App entry point
├── web/
│ └── packages/
│ └── my-app/
│ ├── src/
│ │ └── index.ts # Vue MFE entry
│ └── vite.config.ts
├── config/
│ ├── cert.pem # App certificate
│ └── key.pem # Private key for signing
├── package.json
└── tsconfig.json
IMPORTANT: The config/ directory contains ONLY cert.pem and key.pem for manifest signing. Do NOT create application configuration files (.yml, .json, etc.) here. All app-level configuration must be defined as manifest settings in onCreate().
4.2 Backend Implementation
// src/index.ts
import { resolve } from 'path'
import {
createApp,
defineManifest,
loadCertificate,
loadPrivateKey,
collectViteAssets,
} from '@easy/appserver-sdk'
import type { App } from '@easy/appserver-sdk'
// Configuration via environment variables
const APP_NAME = process.env.APP_NAME || 'de.easy-m.my-app'
const CERT_PATH = process.env.CERT_PATH || './config/cert.pem'
const KEY_PATH = process.env.KEY_PATH || './config/key.pem'
// IMPORTANT: These are the ONLY file paths needed (cert and key for signing)
// All other app configuration must be manifest settings - NOT config files
// DELETE any config file loading code from your v1 app
const app: App = {
async onCreate(ctx) {
// Load certificate
const certificate = await loadCertificate(CERT_PATH)
// Load private key for asset signing
const privateKeyBuffer = await loadPrivateKey(KEY_PATH)
// Collect Vue assets
const uiAssets = await collectViteAssets({
projectDir: resolve(__dirname, '../web/packages/my-app'),
distDir: 'dist',
privateKey: privateKeyBuffer,
skipBuild: false,
buildCommand: 'build',
packageManager: 'pnpm',
skipInstall: true,
})
// Build manifest
let builder = defineManifest()
.name(APP_NAME)
.certificate(certificate)
.privateKey(KEY_PATH)
.ui(ui => ui
.mode('ESM_COMPONENT')
.entryAsset('remoteEntry.js')
.routeBase('/apps/myapp')
.exposedModule('MyApp')
)
// Add assets
for (const asset of uiAssets.assets) {
builder = builder.asset({
name: asset.name,
mimeType: asset.mimeType,
contents: asset.contents,
sha256: asset.sha256,
signature: asset.signature,
})
}
return builder.build()
},
async onRegister(ctx) {
ctx.logger.info('App registered with AppServer')
},
async onInstall(ctx) {
ctx.logger.info('App installed')
},
async onUninstall(ctx) {
ctx.logger.info('App uninstalled')
},
async onDestroy(ctx, reason) {
ctx.logger.info('App destroyed', { reason })
},
}
createApp({ app })
4.3 Environment Variables
# App identity
APP_NAME=de.easy-m.my-app
CERT_PATH=./config/cert.pem
KEY_PATH=./config/key.pem
# AppServer connection
APPSERVER_GRPC_ADDRESS=localhost:9090
4.4 Package.json
{
"name": "de.easy-m.my-app",
"type": "module",
"scripts": {
"start": "tsx src/index.ts",
"build": "tsc",
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"@easy/appserver-sdk": "^0.2.1"
},
"devDependencies": {
"@types/node": "^22.15.3",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=18.0.0"
}
}
5. Frontend Migration (Vue)
Frontend migration is identical regardless of Go or Node.js backend.
5.1 Entry Point Migration
Before (v1) - Standalone app:
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// Listen for shell events
window.addEventListener('core5$session', (e: CustomEvent) => {
const session = e.detail
// handle session
})
window.addEventListener('app$config', (e: CustomEvent) => {
const config = e.detail
// handle config
})
createApp(App)
.use(router)
.mount('#app')
After (v2) - Module Federation:
// index.ts
import type { App as VueApp, Component } from 'vue'
import { createApp, defineAsyncComponent } from 'vue'
import { createPinia, setActivePinia } from 'pinia'
import {
createModuleFederationApp,
type ShellMountOptions,
} from '@easy/appserver-frontend-sdk/adapters'
import { createAppServerPlugin } from '@easy/appserver-frontend-sdk/vue'
export default createModuleFederationApp(() => {
let app: VueApp | null = null
const pinia = createPinia()
return {
async bootstrap() {
// One-time setup (optional)
},
async mount(options: ShellMountOptions) {
// 1. Initialize SDK FIRST
const { getSDK } = await import('@easy/appserver-frontend-sdk')
const sdk = getSDK()
await sdk.initialize({
appName: options.appName,
session: options.session,
settings: options.settings,
apiBaseUrl: options.apiBaseUrl,
})
// 2. Activate Pinia before any store code runs
setActivePinia(pinia)
// 3. Initialize session provider from @easy/frontend-sdk
const { provideSessionUser, useSessionUser } = await import('@easy/frontend-sdk')
provideSessionUser()
if (options.session) {
useSessionUser().setSessionData({
userId: options.session.legacyUserId?.toString() || '',
loginId: options.session.legacyLoginId || 0,
loginType: options.session.legacyLoginType || 'adm',
alias: options.session.email || '',
userLanguage: navigator.language || 'en',
easyUser: options.session.roles.includes('easy'),
})
}
// 4. Lazy load everything else with dynamic imports
const [
{ default: App },
{ default: router },
{ default: i18n },
{ VueComponents, WebComponents },
] = await Promise.all([
import('./App.vue'),
import('./router'),
import('./i18n'),
import('@easy/ui'),
])
// 5. Install web components
WebComponents.EasyUI.install()
// 6. Create and configure Vue app
app = createApp(App)
app.use(pinia)
app.use(createAppServerPlugin({
appName: options.appName,
session: options.session,
settings: options.settings,
apiBaseUrl: options.apiBaseUrl,
}))
app.use(router)
app.use(i18n)
// 7. Register global components
app.component('EzIcon', VueComponents.Icon as Component)
app.component('EzUiProvider', VueComponents.UiProvider)
// 8. Mount to shell-provided container
app.mount(options.container)
},
async unmount() {
if (app) {
app.unmount()
app = null
}
},
async update() {
// Handle prop updates from shell
},
}
})
5.2 Settings Migration
CRITICAL - You MUST:
- DELETE all
app$configevent listeners from your frontend - DELETE any
use-config.tsfiles that read from config events - REPLACE with
useAppSettings()composables from the SDK
The shell provides settings to apps at mount time - there are no config events in v2.
Before (v1) - DOM Events (DELETE THIS):
// use-config.ts
const entityManagerEndpoint = ref('')
document.addEventListener('app$config', (e) => {
const ev = e as CustomEvent
const entityManagerEndpoint = ev.detail.entityManagerEndpoint
if (entityManagerEndpoint) {
Configuration.getInstance().setEntityManagerEndpoint(
new URL(entityManagerEndpoint)
)
entityManagerEndpointRef.value = entityManagerEndpoint
}
})
After (v2) - SDK Composables:
// use-config.ts
import { computed } from 'vue'
import { useAppSettings } from '@easy/appserver-frontend-sdk/vue'
// Settings from manifest - accessed reactively
export function useSettings() {
const settings = useAppSettings()
return {
featureEnabled: computed(() =>
(settings.value?.feature_enabled as boolean) ?? false
),
maxItems: computed(() =>
(settings.value?.max_items as number) ?? 100
),
}
}
Note: Endpoint settings like entityManagerEndpoint are often no longer needed because the HTTP proxy handles routing automatically.
5.3 Remove ConfigGuard Components
ConfigGuard components must be REMOVED entirely. Do not simplify them - delete them.
Why ConfigGuards are no longer needed:
- The HTTP proxy handles endpoint routing automatically
- Frontend doesn't need to know where backends are hosted
- The shell ensures app settings are provided before mounting
- There are no
app$configevents to wait for
Before (v1): Apps used ConfigGuard wrapper components to wait for app$config events before rendering children.
After (v2): DELETE ConfigGuard components. The shell guarantees settings are available at mount time.
If you need conditional rendering based on optional settings, use a simple computed() property inside your component - do NOT create wrapper components:
<!-- Example: Conditional rendering WITHOUT a ConfigGuard wrapper -->
<template>
<MyFeature v-if="featureEnabled" />
<div v-else>Feature not configured</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useAppSettings } from '@easy/appserver-frontend-sdk/vue'
const settings = useAppSettings()
const featureEnabled = computed(() => !!settings.value?.my_optional_setting)
</script>
5.4 GraphQL Client Migration
Before (v1) - Direct fetch with hardcoded endpoint:
// client.ts
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client/core'
const httpLink = createHttpLink({
uri: entityManagerEndpoint, // From app$config event
fetch: (uri, options) => {
return fetch(uri, {
...options,
headers: {
...options?.headers,
'Authorization': `Bearer ${getToken()}`,
},
})
},
})
After (v2) - SDK fetch through proxy (use @easy/shared):
// client.ts
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client/core'
import { createSDKFetch, createOwnAppSDKFetch } from '@easy/shared'
// For cross-app requests (e.g., to entity-manager)
// Routes through /api/apps/de.easy-m.entity-manager/db/query
const entityManagerLink = createHttpLink({
uri: '/db/query',
fetch: createSDKFetch('de.easy-m.entity-manager', '/db/query'),
})
// For own app requests
// Routes through /api/apps/{ownAppName}/query
const ownAppLink = createHttpLink({
uri: '/query',
fetch: createOwnAppSDKFetch('/query'),
})
The HTTP proxy automatically routes requests - frontend doesn't need to know backend URLs.
6. Vite Configuration
All frontend apps must configure Vite to externalize shell-provided dependencies.
6.1 Required Dependencies
pnpm add -D rollup-plugin-external-globals
6.2 Vite Config Template
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import externalGlobals from 'rollup-plugin-external-globals'
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
formats: ['es'], // ES Module only for dynamic import
fileName: () => 'remoteEntry.js',
},
rollupOptions: {
// CRITICAL: External dependencies provided by shell as window globals
external: [
'@easy/ui',
'vue',
'vue-demi',
'@easy/core-essentials',
'@easy/frontend-sdk',
'@easy/appserver-frontend-sdk',
// Add app-specific externals:
// '@easy/code-editor', // if used
],
plugins: [
externalGlobals({
'vue': 'Vue',
'@easy/ui': 'easyUI',
'@easy/core-essentials': 'EzFrontendCoreEssentialsLib',
'@easy/frontend-sdk': 'EzFrontendSDK',
'@easy/appserver-frontend-sdk': 'EzAppserverFrontendSDK',
// '@easy/code-editor': 'EzCodeEditor', // if used
}, {
// Handle vue-demi re-exports
exclude: [/node_modules\/vue-demi/],
}),
],
output: {
globals: {
'@easy/ui': 'easyUI',
'vue': 'Vue',
'@easy/core-essentials': 'EzFrontendCoreEssentialsLib',
'@easy/frontend-sdk': 'EzFrontendSDK',
'@easy/appserver-frontend-sdk': 'EzAppserverFrontendSDK',
},
assetFileNames: (assetInfo) => {
if (assetInfo.names?.[0]?.endsWith('.css')) {
return 'style.css'
}
return assetInfo.names?.[0] || '[name][extname]'
},
},
},
sourcemap: true,
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
})
6.3 External Dependencies Reference
| Package | Window Global | Notes |
|---|---|---|
vue | Vue | Required |
@easy/ui | easyUI | Required |
@easy/core-essentials | EzFrontendCoreEssentialsLib | Required |
@easy/frontend-sdk | EzFrontendSDK | Required |
@easy/appserver-frontend-sdk | EzAppserverFrontendSDK | Required |
@easy/code-editor | EzCodeEditor | If used |
7. Settings Types Reference
CONFIGURATION IN V2 - SUMMARY:
| What | v1 Approach | v2 Approach | Action Required |
|---|---|---|---|
| App settings (feature flags, limits) | Config files (.yml) | Manifest settings | DELETE config files |
| Frontend config | app$config events | useAppSettings() | DELETE event listeners |
| Config helpers | use-config.ts files | SDK composables | DELETE these files |
| ConfigGuard components | Wait for events | Not needed | DELETE entirely |
| Infrastructure (DB, gRPC) | Env vars | Env vars | Keep as-is |
| Certificates | /config/cert.pem | /config/cert.pem | Keep as-is |
Settings defined in the manifest are the ONLY way to configure app-specific behavior. Config files are NOT supported in v2.
Infrastructure environment variables (APPSERVER_GRPC_ADDRESS, APP_ENV, database connections) are still used for deployment configuration.
All Supported Types
| Type | Description | Example Use |
|---|---|---|
string | Optional string | API keys, labels |
string! | Required string | Must be provided |
int | Optional integer | Timeouts, limits |
int! | Required integer | Must be provided |
float | Optional float | Thresholds |
float! | Required float | Must be provided |
bool | Optional boolean | Feature flags |
bool! | Required boolean | Must be provided |
string[] | String array | Tags, categories |
int[] | Integer array | Port lists |
float[] | Float array | Numeric lists |
bool[] | Boolean array | Multiple toggles |
json | Optional JSON object | Complex config |
json! | Required JSON | Must be provided |
KVPair[] | Key-value pairs | Custom mappings |
Example with Validation
Go Example:
builder.Setting(&manifest.SettingConfig{
Key: "max_items",
Type: "int",
Label: "Maximum Items",
Description: "Maximum number of items to display",
Default: 100,
Validation: &manifest.ValidationConfig{
Min: ptr(1),
Max: ptr(1000),
},
UIHints: &manifest.UIHintsConfig{
InputType: ptr("number"),
HelpText: ptr("Enter a value between 1 and 1000"),
},
})
Node.js Example:
builder.setting({
key: 'allowed_domains',
type: 'string[]',
label: 'Allowed Domains',
description: 'List of allowed domain patterns',
default: ['*.example.com'],
})
8. Migration Checklist
Backend (Go)
- Update imports from
de.easy-m.sdk-gotoeasy.appserver/pkg/v2/sdk - Implement
sdk.Appinterface (change from interface embedding) - Rename
GetID()toName()method - Add
*sdk.LifecycleContextparameter to all lifecycle hooks - Move manifest creation to
OnCreate()using builder pattern - Remove manual database init (use
ctx.DBprovided by SDK) - Define settings in manifest (replaces ALL config files)
- Configure UI builder for Module Federation
- Add asset collection for frontend apps
- Update
main.goentry point - Set up logger adapter for v1 compatibility (
globallogger.Set()) - Migrate entity manager client to v2 SDK (
entitymanager.NewClient()) - Update
bitbucket-pipelines.ymlwith v2 build steps - DELETE all
config/*.ymlorconfig/*.yamlfiles from repository - DELETE all config file loading/parsing code (viper, yaml.Unmarshal, etc.)
- VERIFY no config files are referenced in Makefile or CI/CD
Backend (Node.js)
- Create
src/index.tswithcreateApp({ app }) - Implement
Appinterface with lifecycle hooks - Configure environment variables (APP_NAME, CERT_PATH, KEY_PATH only)
- Use
collectViteAssets()for frontend assets - Build manifest with
defineManifest() - Update
package.jsonwith SDK dependencies - DELETE any config file loading code from v1 app
- DELETE any
config/*.jsonorconfig/*.ymlfiles (keep only cert/key)
Frontend (Vue)
- Create
src/index.tswithcreateModuleFederationApp() - DELETE all
app$configevent listeners - DELETE
use-config.tsor similar config-reading files - DELETE ConfigGuard components entirely
- REPLACE config access with
useAppSettings()composables - Update Vite config with externals and
externalGlobals - Update GraphQL clients to use
createSDKFetch()from@easy/shared - Keep
main.tsfor standalone development/testing only
9. V1-Compatible Adapters
The v2 SDK provides adapter wrappers that maintain compatibility with existing v1 interfaces. This allows gradual migration while reusing existing business logic.
9.1 Logger Port Adapter
If your app uses loggerport.Logger from core-libs, wrap the v2 SDK logger:
File: pkg/v2/sdk/telemetry/loggerport_adapter.go
import (
"bitbucket.org/easymarketinggmbh/easy.appserver/pkg/v2/sdk/telemetry"
"bitbucket.org/easymarketinggmbh/core-libs/pkg/loggerport"
"bitbucket.org/easymarketinggmbh/core-libs/pkg/globallogger"
)
func (app *MyApp) OnStartUp(ctx *sdk.LifecycleContext) error {
// Initialize old SDK's global logger for backward compatibility
// with internal packages that still use globallogger.L()
globallogger.Set(telemetry.NewLoggerAdapter(ctx.Logger))
return nil
}
The adapter converts v2 telemetry fields to v1 loggerport fields and maps log levels:
Debug,Info,Warn,Error→ mapped directlyFatal,Panic→ mapped toError(v2 SDK doesn't have fatal/panic)
9.2 Entity Manager Client
The v2 SDK entity manager client implements the same interface as v1, enabling drop-in replacement:
import (
"bitbucket.org/easymarketinggmbh/easy.appserver/pkg/v2/sdk/entitymanager"
)
func (app *MyApp) OnInstall(ctx *sdk.LifecycleContext) error {
// Initialize entity manager client for communicating with entity-manager app
emClient, err := entitymanager.NewClient(&entitymanager.ClientOptions{
AppServerURL: ctx.GetAppServerURL(),
AppName: app.name,
PrivateKeyPath: ctx.GetPrivateKeyPath(),
})
if err != nil {
return fmt.Errorf("failed to create entity manager client: %w", err)
}
// Use with existing adapters that expect EntityManager interface
app.widgetAdapter = adapters.NewWidgetInstanceAdapter(emClient)
return nil
}
9.3 EntityManager Interface Compatibility
The v2 entitymanager.EntityManager interface matches v1:
type EntityManager interface {
Query(ctx context.Context, q any, opts ...EntityManagerRequestOption) error
Mutate(ctx context.Context, q any, opts ...EntityManagerRequestOption) error
SetSignature(signature string)
Signature() string
}
9.4 Using Adapters in Business Logic
Existing port/adapter patterns work unchanged:
// Your existing adapter (unchanged)
type WidgetInstanceAdapter struct {
entityManager entitymanager.EntityManager // Same interface as v1
}
func (a *WidgetInstanceAdapter) FetchOne(ctx context.Context, input FetchWidgetInput) (*Widget, error) {
var query widgetQuery
// Works with both v1 and v2 entity manager clients
err := a.entityManager.Query(ctx, &query, entitymanager.WithVariables(map[string]any{
"id": graphql.ID(input.ID.String()),
}))
if err != nil {
return nil, err
}
return mapToEntity(query), nil
}
9.5 Authentication Transport
The v2 entity manager client handles inter-app authentication automatically via AuthTransport:
- Signs requests with RSA-SHA256 (
X-App-Signatureheader) - Sets app identity (
X-App-Name,X-App-Timestamp) - Propagates session ID via
Authorizationheader - Forwards request IDs for tracing
No changes needed in your adapter code - authentication is handled internally.
10. Testing
10.1 Standalone Development
Keep main.ts for standalone development and testing:
// main.ts (for dev only, not used in production)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
10.2 Mocking SDK in Tests
// In tests, mock the SDK
vi.mock('@easy/appserver-frontend-sdk', () => ({
getSDK: () => ({
initialize: vi.fn(),
getSettings: () => ({ backend_endpoint: '/test' }),
getSession: () => ({ userId: 'test' }),
fetch: vi.fn(),
}),
}))
11. Database Migrations
11.1 Migration Architecture
v2 SDK provides automatic database migration support with per-app schema isolation:
- Schema Naming: Auto-derived from app name (e.g.,
de.easy-m.notes→app_de_easy_m_notes) - Execution: SDK runs migrations automatically before
OnStartUp()hook - Tracking: Each app has its own
schema_migrationstable
11.2 Migration File Conventions
Pattern: {version}_{name}.{up|down}.sql
Rules:
- Version: 3+ digits (e.g.,
001,002,100) - Name: lowercase letters, numbers, underscores only
- Direction:
.up.sqlor.down.sqlrequired
Valid Examples:
001_create_users.up.sql
001_create_users.down.sql
002_add_email_index.up.sql
002_add_email_index.down.sql
Invalid Examples:
01_create_users.up.sql # version too short
001_create-users.up.sql # hyphen in name
001_CreateUsers.up.sql # uppercase in name
001_create_users.sql # missing direction
11.3 Go SDK Migration Setup
package myapp
import (
"embed"
"bitbucket.org/easymarketinggmbh/easy.appserver/pkg/v2/sdk/manifest"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
func (app *MyApp) OnCreate(ctx *sdk.LifecycleContext) (*manifest.Manifest, error) {
return manifest.DefineManifest().
Name(app.name).
Certificate(certData).
PrivateKey(keyPath).
Migrations(migrationsFS, "migrations"). // Add this line
// ... rest of manifest
Build()
}
Custom Schema Name (optional):
.MigrationsWithSchema(migrationsFS, "migrations", "custom_schema_name")
11.4 Migration Tracking Table
The SDK automatically creates a tracking table per app:
CREATE TABLE IF NOT EXISTS {schema}.schema_migrations (
version BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
checksum VARCHAR(64) NOT NULL,
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
execution_ms INTEGER NOT NULL,
rolled_back_at TIMESTAMP
);
11.5 Migration Features
| Feature | Description |
|---|---|
| Checksum Validation | SHA-256 of up+down SQL ensures migrations haven't changed |
| Advisory Locks | PostgreSQL locks prevent concurrent migrations |
| Transaction Safety | Each migration runs in its own transaction |
| Auto-Rollback | Failed migrations automatically roll back |
| Schema Isolation | Each app has its own PostgreSQL schema |
11.6 Example Migration Files
migrations/001_create_todos.up.sql:
CREATE TABLE todos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_todos_created_at ON todos(created_at);
migrations/001_create_todos.down.sql:
DROP INDEX IF EXISTS idx_todos_created_at;
DROP TABLE IF EXISTS todos;
12. Docker/Deployment Migration
12.1 Dockerfile Strategy Changes
| Aspect | v1 | v2 |
|---|---|---|
| Build Location | In Docker container | In CI/CD pipeline |
| Base Image | golang:1.18-buster + distroless | Scratch or minimal Alpine |
| Binary | Compiled in Docker | Pre-built, copied in |
| SSH Keys | Passed to Docker build | Not needed |
| Startup Time | Slower (compile) | Faster (just copy) |
| Image Size | Larger | Minimal |
12.2 v1 Dockerfile Pattern (Build in Docker)
# v1 - Multi-stage build with compilation
FROM golang:1.18-buster AS builder
# SSH key setup for private repos
ARG SSH_PRIVATE_KEY
RUN mkdir -p /root/.ssh && \
echo "$SSH_PRIVATE_KEY" > /root/.ssh/id_rsa && \
chmod 600 /root/.ssh/id_rsa
WORKDIR /app
COPY . .
RUN go build -o /app/bin/main ./cmd/app
# Runtime stage
FROM gcr.io/distroless/base-debian10:debug
COPY --from=builder /app/bin/main /app/main
COPY --from=builder /app/config /config
USER nonroot:nonroot
EXPOSE 8081 8082
ENTRYPOINT ["/app/main"]
12.3 v2 Dockerfile Pattern (Pre-built Binary)
Important: In v2, web assets should be pre-built in CI/CD before Docker image creation. This allows the manifest builder to skip web-install and web-build steps at runtime and just collect the pre-built assets.
Path Resolution: The SDK's ResolvePath() helper converts relative paths to absolute paths in production:
- Development:
config/key.pem→config/key.pem - Production:
config/key.pem→/config/key.pem
This means your Dockerfile must place files at the root level:
// In OnCreate(), paths are resolved and build is skipped in production
result, err := collectors.CollectMakeAssets(&collectors.MakeAssetCollectorOptions{
PrivateKeyPath: sdk.ResolvePath(ctx, "config/key.pem"), // /config/key.pem in production
DistDir: sdk.ResolvePath(ctx, "web/packages/my-app/dist"), // /web/packages/... in production
SkipInstall: sdk.IsProduction(ctx), // Skip - assets pre-built in CI/CD
SkipBuild: sdk.IsProduction(ctx), // Skip - assets pre-built in CI/CD
})
Option A: Scratch Image (Minimal)
# v2 - Pre-built binary, minimal image
FROM alpine:latest AS certs
RUN apk --no-cache add ca-certificates
FROM scratch
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Pre-built binary from CI/CD
COPY main /main
# Certificate/key files ONLY - no app config files (sdk.ResolvePath resolves paths)
# Contains only cert.pem and key.pem for manifest signing
COPY config /config
# Web assets at ROOT level, PRE-BUILT in CI/CD
COPY web/packages/my-app/dist /web/packages/my-app/dist
CMD ["/main"]
Option B: Debian Base (For debugging)
# v2 - Debian base for debugging capabilities
FROM debian:bookworm-slim
COPY main /main
COPY config /config
CMD ["/main"]
12.4 Node.js App Dockerfile (v2)
Important: Web assets are pre-built in CI/CD. The Node.js SDK's collectViteAssets() can then skip building:
const uiAssets = await collectViteAssets({
projectDir: resolve(__dirname, '../web/packages/my-app'),
distDir: 'dist',
privateKey: privateKeyBuffer,
skipBuild: true, // Assets already built in CI/CD
skipInstall: true, // Dependencies already installed
})
Note: Node.js apps use /app as working directory, so paths are relative to /app:
FROM node:22-alpine
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile --prod
# Copy application
COPY dist ./dist
COPY config ./config
# Web assets PRE-BUILT in CI/CD (SDK just collects these)
COPY web/packages/my-app/dist ./web/packages/my-app/dist
CMD ["node", "dist/index.js"]
12.5 CI/CD Build Pipeline Changes
Before (v1): Build happens in Docker
# v1 pipeline
build:
script:
- docker build -t myapp:latest --build-arg SSH_PRIVATE_KEY="$SSH_KEY" .
After (v2): Build before Docker
# v2 pipeline
build:
script:
# Build binary outside Docker
- go build -o main ./cmd/app
# Build frontend assets
- cd web && pnpm install && pnpm build
# Create minimal Docker image
- docker build -t myapp:latest .
12.6 Environment Variables
IMPORTANT: Do NOT add app-specific environment variables to Docker Compose. The only environment variables needed are:
- Infrastructure:
APPSERVER_GRPC_ADDRESS,APP_ENV - Database:
POSTGRES_*variables (for db-migrate service only)
All app-specific configuration (feature flags, limits, API endpoints) must be manifest settings, configured via the AppServer admin UI - NOT environment variables or config files.
Infrastructure (Docker Compose):
services:
db-migrate:
environment:
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- POSTGRES_USER=partner
- POSTGRES_PASSWORD=changeme
- POSTGRES_DB=appserver
- POSTGRES_SCHEMA=appserver
- POSTGRES_SSLMODE=disable
my-app:
environment:
# ONLY infrastructure env vars - NO app-specific configuration here
- APP_ENV=production
- APPSERVER_GRPC_ADDRESS=appserver:9090
App-specific environment variables from v1 are now replaced by manifest settings (see Section 7). DELETE them from your Docker Compose files.
12.7 Docker Compose Service Pattern
services:
my-app:
image: my-app:latest
volumes:
- ./config:/config:ro
environment:
- APP_ENV=production
depends_on:
- postgres
- appserver
networks:
- appserver-network
12.8 Bitbucket Pipelines Migration
The bitbucket-pipelines.yml file needs updates for v2. Reference files:
de.easy-m.statistics/bitbucket-pipelines.yml(Go + Vue)de.easy-m.tagmanagement/bitbucket-pipelines.yml(Node.js + Vue)de.easy-m.entity-manager/bitbucket-pipelines.yml(Go only)de.easy-m.data-sources/bitbucket-pipelines.yml(Go + Vue)
Common Patterns (All Apps)
1. Self-hosted runners and private repo access:
- step: &BuildV2
runs-on:
- "self.hosted"
script:
- export GOPRIVATE=bitbucket.org/easymarketinggmbh
- git config --global url."git@bitbucket.org:easymarketinggmbh".insteadOf "https://bitbucket.org/easymarketinggmbh"
- npm config set //npm.eacore6.de/:_authToken "$NPM_PUBLISH_TOKEN"
2. V2 branch detection and tagging:
script:
- DEPLOY_TAG="latest";
- if [ ${BITBUCKET_BRANCH} == "testing" ]; then
DEPLOY_TAG="testing";
elif [ ${BITBUCKET_BRANCH} == "pre-release" ]; then
DEPLOY_TAG="pre-release";
# NEW: v2 branch support
elif [ ${BITBUCKET_BRANCH} == "feature/appserver-v2" ] || [ ${BITBUCKET_BRANCH} == "feature/v2" ]; then
DEPLOY_TAG="rc-v2";
fi
3. Add v2 branch pipelines:
branches:
feature/appserver-v2:
- step: *BuildV2
- step: *Create-push-image
feature/v2:
- step: *BuildV2
- step: *Create-push-image
App-Specific Patterns
Go + Vue Apps (statistics, data-sources):
- step: &BuildV2
script:
- make -B build-v2 # Changed from build-prod
- make -B web-install # Build frontend deps
- make -B web-build # Build frontend assets
- chmod 777 ./bin/*/*
artifacts:
- bin/**
- config/** # Include config (data-sources)
- web/packages/my-app/dist/** # Pre-built frontend assets
Pure Node.js Apps (tagmanagement):
- step: &Build-v2
script:
- npm install -g pnpm
- npm config set //npm.eacore6.de/:_authToken "$NPM_PUBLISH_TOKEN"
- pnpm install --frozen-lockfile
- cd web && pnpm install --frozen-lockfile && pnpm build && cd ..
artifacts:
- node_modules/**
- web/packages/tagmanagement-app/dist/**
- web/packages/tagmanagement-core/dist/**
- web/packages/tagmanagement-web-adapters/dist/**
Go Only Apps (entity-manager):
# Uses SSH remote builds - passes branch parameter in v2
- step: &Build-Binaries-v2
script:
- ssh -o ... "cd ... && git pull && git checkout ${BITBUCKET_BRANCH} && make build-v2"
Docker Build Command Change
# v1: Uses relative path with file copying
- docker build build/package/app/ --label "version=..."
# v2: Uses Dockerfile reference (Dockerfile handles copying)
- docker build -f build/package/app/Dockerfile . --label "version=..."
Pipeline Comparison Matrix
| App | Build Type | V2 Build Command | Artifacts Added |
|---|---|---|---|
| statistics | Go + Vue | make build-v2 + web-install + web-build | web/packages/*/dist/** |
| data-sources | Go + Vue | make build-prod (same) | config/** |
| tagmanagement | Node.js | pnpm install && pnpm build | Multiple dist/** packages |
| entity-manager | Go only | SSH remote + branch param | No web artifacts |
Summary of Changes
| Aspect | v1 | v2 |
|---|---|---|
| Build target | make -B build-prod | make -B build-v2 (or pnpm for Node.js) |
| Frontend build | Separate CDN push step | Integrated in build step |
| Docker build | Manual file copying | Dockerfile-based |
| CGO | CGO_ENABLED=1 | CGO_ENABLED=0 |
| Artifacts | Binary only | Binary + config + web dist |
| Docker tag | Branch-specific | rc-v2 for all v2 branches |
13. App-Specific Notes
13.1 Entity Manager (Pure Go Backend)
- No frontend assets needed
- Focus on API routes in manifest
- Database migrations use
Migrations()builder method - Handles both PostgreSQL and MongoDB connections
13.2 Statistics / Data-Sources (Go + Vue)
- Uses
collectors.CollectMakeAssets()for Vite assets - Both entity-manager client AND own-app client needed
- ConfigGuard component updated for SDK composables
- Assets bundled in Docker image at
/web/packages/*/dist
13.3 Tagmanagement (Node.js + Vue)
- Replaced Go
app.goentirely with Node.js SDK - Uses
collectViteAssets()from@easy/appserver-sdk - GraphQL client uses
createSDKFetch()from@easy/shared - Node.js 22+ required for SDK
14. Common Pitfalls
14.1 Using Wrong SDK Fetch
Problem: Creating custom fetch wrappers instead of using @easy/shared
Solution: Use createSDKFetch() and createOwnAppSDKFetch() from @easy/shared
14.2 Missing Vite Externals
Problem: Vue/UI components not available at runtime
Solution: Add to BOTH external AND externalGlobals in vite.config.ts
14.3 Settings Not Available
Problem: useAppSettings() returns empty/undefined
Solution: Ensure sdk.initialize() is called before accessing settings in mount()
14.4 Asset Signature Failures
Problem: Assets rejected by AppServer
Solution: Ensure private key matches certificate, use AssetWithSignature
14.5 SDK Not Initialized in Frontend
Problem: getSDK() returns uninitialized SDK
Solution: Call initialize() at mount time with shell-provided options BEFORE any other imports
14.6 GraphQL Requests Failing
Problem: 401/403 errors on GraphQL requests Solution: Use SDK fetch wrapper - requests must go through AppServer proxy for authentication
14.7 Dynamic Imports Not Working
Problem: Module not found errors at runtime
Solution: Ensure all deferred imports are inside mount() function, not at module level
14.8 Logger Adapter Not Set
Problem: globallogger.L() returns nil or causes panic
Solution: Set the logger adapter in OnStartUp:
globallogger.Set(telemetry.NewLoggerAdapter(ctx.Logger))
14.9 Entity Manager Authentication Errors
Problem: 401/403 errors when calling entity-manager
Solution: Ensure PrivateKeyPath is correct and matches the certificate in the manifest