Skip to main content

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

AspectAppServer v1AppServer v2
Backend ModelInterface embedding (easysdk.App)Interface implementation (sdk.App)
Backend ConfigurationConfig files + environment variablesManifest settings + environment variables
Frontend Configurationapp$config DOM eventsSDK composables
Frontend IntegrationStandalone app with event listenersModule Federation (MFE)
HTTP RequestsDirect fetch with auth tokensSDK fetch through AppServer proxy
Asset DeliverySeparate static hostingSigned assets in manifest
Inter-App CommunicationDirect HTTP callsAppServer-mediated proxy with signatures

1.2 SDK Package Reference

PurposeGo SDKNode.js SDK
Core SDKeasy.appserver/pkg/v2/sdk@easy/appserver-sdk
Manifest buildereasy.appserver/pkg/v2/sdk/manifestdefineManifest() from SDK
Asset collectionsdk/manifest/collectorscollectViteAssets() from SDK
Frontend SDKN/A@easy/appserver-frontend-sdk
Frontend composablesN/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 Typev1 Approachv2 ApproachMigration Action
App settings (feature flags, limits, endpoints)config/*.yml filesManifest settingsDELETE config files
Frontend configurationapp$config DOM eventsuseAppSettings() composableDELETE event listeners
Frontend config helpersuse-config.ts filesSDK composablesDELETE these files
ConfigGuard componentsWait for app$configNot neededDELETE entirely
Infrastructure (DB, gRPC address)Environment variablesEnvironment variablesKeep as-is
Certificates for signing/config/cert.pem, /config/key.pem/config/cert.pem, /config/key.pemKeep as-is

What you MUST delete when migrating:

  • All config/*.yml or config/*.yaml files (app configuration)
  • All config file loading/parsing code in your backend
  • All app$config event listeners in your frontend
  • All use-config.ts or similar config-reading files
  • All ConfigGuard wrapper components

What you keep:

  • /config/cert.pem and /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

HookWhen CalledPurpose
OnStartUp(ctx)Before networkEarly setup (DB available via ctx.DB)
OnCreate(ctx)After startupBuild and return manifest
OnRegister(ctx)After registrationPost-registration setup
OnInstall(ctx)App installedService initialization
OnUninstall(ctx)App uninstalledCleanup resources
OnDestroy(ctx, reason)ShutdownFinal 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:

  1. DELETE all app$config event listeners from your frontend
  2. DELETE any use-config.ts files that read from config events
  3. 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$config events 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

PackageWindow GlobalNotes
vueVueRequired
@easy/uieasyUIRequired
@easy/core-essentialsEzFrontendCoreEssentialsLibRequired
@easy/frontend-sdkEzFrontendSDKRequired
@easy/appserver-frontend-sdkEzAppserverFrontendSDKRequired
@easy/code-editorEzCodeEditorIf used

7. Settings Types Reference

CONFIGURATION IN V2 - SUMMARY:

Whatv1 Approachv2 ApproachAction Required
App settings (feature flags, limits)Config files (.yml)Manifest settingsDELETE config files
Frontend configapp$config eventsuseAppSettings()DELETE event listeners
Config helpersuse-config.ts filesSDK composablesDELETE these files
ConfigGuard componentsWait for eventsNot neededDELETE entirely
Infrastructure (DB, gRPC)Env varsEnv varsKeep as-is
Certificates/config/cert.pem/config/cert.pemKeep 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

TypeDescriptionExample Use
stringOptional stringAPI keys, labels
string!Required stringMust be provided
intOptional integerTimeouts, limits
int!Required integerMust be provided
floatOptional floatThresholds
float!Required floatMust be provided
boolOptional booleanFeature flags
bool!Required booleanMust be provided
string[]String arrayTags, categories
int[]Integer arrayPort lists
float[]Float arrayNumeric lists
bool[]Boolean arrayMultiple toggles
jsonOptional JSON objectComplex config
json!Required JSONMust be provided
KVPair[]Key-value pairsCustom 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-go to easy.appserver/pkg/v2/sdk
  • Implement sdk.App interface (change from interface embedding)
  • Rename GetID() to Name() method
  • Add *sdk.LifecycleContext parameter to all lifecycle hooks
  • Move manifest creation to OnCreate() using builder pattern
  • Remove manual database init (use ctx.DB provided by SDK)
  • Define settings in manifest (replaces ALL config files)
  • Configure UI builder for Module Federation
  • Add asset collection for frontend apps
  • Update main.go entry point
  • Set up logger adapter for v1 compatibility (globallogger.Set())
  • Migrate entity manager client to v2 SDK (entitymanager.NewClient())
  • Update bitbucket-pipelines.yml with v2 build steps
  • DELETE all config/*.yml or config/*.yaml files 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.ts with createApp({ app })
  • Implement App interface with lifecycle hooks
  • Configure environment variables (APP_NAME, CERT_PATH, KEY_PATH only)
  • Use collectViteAssets() for frontend assets
  • Build manifest with defineManifest()
  • Update package.json with SDK dependencies
  • DELETE any config file loading code from v1 app
  • DELETE any config/*.json or config/*.yml files (keep only cert/key)

Frontend (Vue)

  • Create src/index.ts with createModuleFederationApp()
  • DELETE all app$config event listeners
  • DELETE use-config.ts or 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.ts for 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 directly
  • Fatal, Panic → mapped to Error (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-Signature header)
  • Sets app identity (X-App-Name, X-App-Timestamp)
  • Propagates session ID via Authorization header
  • 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.notesapp_de_easy_m_notes)
  • Execution: SDK runs migrations automatically before OnStartUp() hook
  • Tracking: Each app has its own schema_migrations table

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.sql or .down.sql required

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

FeatureDescription
Checksum ValidationSHA-256 of up+down SQL ensures migrations haven't changed
Advisory LocksPostgreSQL locks prevent concurrent migrations
Transaction SafetyEach migration runs in its own transaction
Auto-RollbackFailed migrations automatically roll back
Schema IsolationEach 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

Aspectv1v2
Build LocationIn Docker containerIn CI/CD pipeline
Base Imagegolang:1.18-buster + distrolessScratch or minimal Alpine
BinaryCompiled in DockerPre-built, copied in
SSH KeysPassed to Docker buildNot needed
Startup TimeSlower (compile)Faster (just copy)
Image SizeLargerMinimal

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.pemconfig/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

AppBuild TypeV2 Build CommandArtifacts Added
statisticsGo + Vuemake build-v2 + web-install + web-buildweb/packages/*/dist/**
data-sourcesGo + Vuemake build-prod (same)config/**
tagmanagementNode.jspnpm install && pnpm buildMultiple dist/** packages
entity-managerGo onlySSH remote + branch paramNo web artifacts

Summary of Changes

Aspectv1v2
Build targetmake -B build-prodmake -B build-v2 (or pnpm for Node.js)
Frontend buildSeparate CDN push stepIntegrated in build step
Docker buildManual file copyingDockerfile-based
CGOCGO_ENABLED=1CGO_ENABLED=0
ArtifactsBinary onlyBinary + config + web dist
Docker tagBranch-specificrc-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.go entirely 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