Skip to content

Core Platform Flows & Use Cases

This page documents the exact sequence of operations for every major flow in MetaOne Core, traced directly from the source code. Step numbers correspond to the actual call order in the implementations.


1. Extension Install Flow

Entry point: POST /api/admin/workspaces/{workspaceId}/extensions
Implementation: ExtensionInstallerServiceImpl.install()
Transactional: yes — entire flow runs in one transaction; registry writes and DB records roll back together on failure.

POST /api/admin/workspaces/{workspaceId}/extensions
  { extensionKey, version }


 1. Workspace existence check
    └── workspaceRepository.existsById(workspaceId)
        └── throws 404 if not found



 2. Fetch manifest from Marketplace
    └── MarketplaceService.getManifest(extensionKey, version)
        └── In dev: MarketplaceServiceStub (in-memory)
        └── throws RuntimeException if not found



 3. Policy check
    └── ExtensionPolicyEvaluator.canInstall(workspaceId, manifest)
        └── throws RuntimeException if denied



 4. Upsert ext_installation row
    └── status = INSTALLING, enabled = false
    └── Existing row is reused on reinstall (idempotent upsert)



 5. Fetch artifact
    └── MarketplaceService.getArtifact(extensionKey, version)



 6. Signature verification
    └── SignatureVerificationService.verify(artifact, manifest)
        └── [currently a stub — always passes]



 7. Compatibility check
    └── CompatibilityService.assertCompatible(manifest)
        └── [currently a stub — always passes]



 8. Deploy runtime (IN_PROCESS only — done BEFORE DB provisioning)
    └── ExtensionRuntimeService.deploy(workspaceId, manifest, artifact)
        └── PF4J: resolves jar at {pluginsPath}/{pluginId}/{version}/{pluginId}-{version}.jar
        └── pluginManager.loadPlugin(path) if not already loaded
        └── pluginManager.startPlugin(pluginId)



 9. Database provisioning
    └── DatabaseProvisioningService.provisionAndMigrate(workspaceId, manifest)
        └── [currently a stub — schema migration not yet implemented]



10. Deploy runtime (EXTERNAL_SERVICE only — done AFTER DB provisioning)
    └── ExtensionRuntimeService.deploy() — no-op for external; confirms service is expected running



11. Health check
    └── ExtensionHealthCheckService.assertHealthy(workspaceId, manifest)
        └── IN_PROCESS: checks PF4J PluginState == STARTED
        └── EXTERNAL_SERVICE: HTTP GET to {serviceBaseUrl}/health
        └── throws on failure — registry writes are NOT reached



12. Registry writes (only if health check passed)
    ├── CapabilityRegistryService.register(workspaceId, manifest)
    │   └── Writes rows to ext_capability_registry
    ├── UiMountRegistryService.register(workspaceId, extensionKey, uiManifest)
    │   └── Writes rows to ext_ui_mount_registry
    └── EventSubscriptionRegistry.registerSubscriptions(workspaceId, extensionKey, consumesEvents)
        └── Writes rows to ext_event_subscription (audit/future use)



13. In-memory event handler registration (IN_PROCESS, AFTER transaction commit)
    └── TransactionSynchronizationManager.afterCommit()
        └── WorkspaceContext.set(workspaceId)
        └── InProcessEventGateway.register(workspaceId, extensionKey)
            └── Scans PF4J extensions for PlatformEventHandler
            └── Indexes @EventHandler methods by eventKey
            └── Idempotent: deregisters old entry first
        └── WorkspaceContext.clear()



14. Config seeding and validation
    ├── ExtensionConfigService.seedDefaults() — writes default values
    └── If manifest has required config fields:
        └── ExtensionConfigService.validateRequiredConfig()
            ├── All fields present → status = INSTALLED, enabled = true
            └── Missing fields    → status = PENDING_CONFIG, enabled = false



15. Save final status + audit log
    └── ext_installation updated (INSTALLED or PENDING_CONFIG)
    └── ext_audit_log: action = INSTALL, actor = current username or "system"

Error path

If any step from 5–13 throws:

  1. extensionRuntimeService.undeploy() is called to remove the ghost plugin
  2. ext_installation.status is set to FAILED with the error message
  3. An FAILURE audit log entry is written
  4. The exception is swallowed — the method returns the FAILED installation record rather than re-throwing

Why swallow? The update() flow inspects the returned status to decide whether to roll back, without needing exception propagation.


2. Extension Uninstall Flow

Entry point: DELETE /api/admin/workspaces/{workspaceId}/extensions/{extensionKey}
Implementation: ExtensionInstallerServiceImpl.uninstall()

 1. Policy check
    └── ExtensionPolicyEvaluator.canManageExtension(workspaceId, extensionKey)

 2. Set status = UNINSTALLING (visible to health monitors)

 3. Registry cleanup (DB)
    ├── CapabilityRegistryService.unregister(workspaceId, extensionKey)
    ├── UiMountRegistryService.unregister(workspaceId, extensionKey)
    ├── EventSubscriptionRegistry.unregisterSubscriptions(workspaceId, extensionKey)
    └── ExtensionConfigService.deleteConfig(workspaceId, extensionKey)

 4. In-memory event handler deregistration (AFTER commit)
    └── TransactionSynchronizationManager.afterCommit()
        └── InProcessEventGateway.deregister(workspaceId, extensionKey)
            └── Removes workspaceId from enabledByWorkspace[extensionKey]
            └── If no workspaces remain → removes handler descriptors from registry map

 5. Runtime undeploy
    └── ExtensionRuntimeService.undeploy(workspaceId, extensionKey)
        └── If extension is still INSTALLED + enabled for another workspace → skip (shared plugin)
        └── Otherwise: pluginManager.stopPlugin() + pluginManager.unloadPlugin()

 6. Delete ext_installation row

 7. Write UNINSTALL audit log

3. Extension Enable / Disable Flow

Entry point:

  • POST /api/admin/workspaces/{workspaceId}/extensions/{extensionKey}/enable
  • POST /api/admin/workspaces/{workspaceId}/extensions/{extensionKey}/disable

Enable

 1. Policy check: ExtensionPolicyEvaluator.canEnable()

 2. Required config validation
    └── If PENDING_CONFIG: validates all required fields present
    └── Missing fields → throws (extension stays PENDING_CONFIG)

 3. If status == PENDING_CONFIG and all config present:
    └── status → INSTALLED

 4. enabled = true, save

 5. Re-deploy runtime
    └── ExtensionRuntimeService.deploy(workspaceId, manifest, artifact)

 6. Re-register in-memory event handlers (IN_PROCESS, after commit)
    └── InProcessEventGateway.register(workspaceId, extensionKey)

 7. Write ENABLE audit log

Disable

 1. Policy check: ExtensionPolicyEvaluator.canManageExtension()

 2. enabled = false, save

 3. Deregister in-memory event handlers (after commit)
    └── InProcessEventGateway.deregister(workspaceId, extensionKey)

 4. Undeploy runtime
    └── ExtensionRuntimeService.undeploy()
        └── Skipped if still INSTALLED+enabled for another workspace

 5. Write DISABLE audit log

4. Extension Update Flow (with Rollback)

Entry point: PUT /api/admin/workspaces/{workspaceId}/extensions/{extensionKey}
Implementation: ExtensionUpdaterServiceImpl.update()

 1. Policy check: ExtensionPolicyEvaluator.canManageExtension()

 2. Load current installation — capture oldVersion, wasEnabled

 3. Call installerService.install(extensionKey, newVersion, workspaceId)
    └── Full install flow (steps 1–15 above) runs inside same transaction
    └── Returns FAILED status instead of throwing on error

 4a. If result.status == FAILED → ROLLBACK PATH:
     ├── Restore installation: version = oldVersion, status = INSTALLED, enabled = wasEnabled
     ├── Re-deploy old version via ExtensionRuntimeService.deploy()
     │   └── If this also fails → status = FAILED, error = "rollback failed"
     ├── Write ext_update_history: status = FAILED, error = "rolled back to {oldVersion}"
     └── Write UPDATE audit log: "Update failed — rolled back"

 4b. If result.status != FAILED → SUCCESS PATH:
     ├── Write ext_update_history: fromVersion, toVersion, status
     └── Write UPDATE audit log: "Updated from {old} to {new}"

Shared transaction note: update() and install() share one Spring transaction (default REQUIRED propagation). If install() were changed to REQUIRES_NEW, the rollback logic would silently break because install's DB writes would commit independently.


5. Capability Invocation Flow

Entry point: POST /api/workspaces/{workspaceId}/capabilities/{capabilityKey}
Implementation: CapabilityInvocationServiceImplCapabilityGatewayFactory

POST /api/workspaces/{workspaceId}/capabilities/{capabilityKey}
  { ...request payload... }


 1. Resolve registration
    └── CapabilityRegistryService.resolveRegistration(workspaceId, capabilityKey)
        └── Queries ext_capability_registry WHERE workspaceId AND capabilityKey
        └── throws "Capability not found" if missing



 2. Get gateway by runtimeType
    └── CapabilityGatewayFactory.getGateway(registration.runtimeType)
        ├── IN_PROCESS       → InProcessCapabilityGateway
        └── EXTERNAL_SERVICE → ExternalCapabilityGateway



 3. Set WorkspaceContext (ThreadLocal)
    └── WorkspaceContext.set(workspaceId)


        ├─── IN_PROCESS path ──────────────────────────────────────────────
        │       │
        │       ▼
        │   4a. Find handler via PF4J
        │       └── pluginManager.getExtensions(CapabilityHandler.class, extensionKey)
        │       └── Filter: class has @CapabilityGroup
        │       └── Match prefix: capabilityKey.startsWith(group.prefix + ".")
        │       └── Match method: @Capability(key) where prefix + "." + key == capabilityKey
        │       └── throws "handler not found" if no match

        │   5a. Validate method signature
        │       └── Must have 1 or 2 parameters: (RequestType) or (RequestType, CapabilityContext)

        │   6a. Type-convert request payload
        │       └── ObjectMapper.convertValue(request, method.getParameterTypes()[0])

        │   7a. Invoke via reflection
        │       ├── method.invoke(handler, request)               — 1-param
        │       └── method.invoke(handler, request, context)      — 2-param

        │   8a. Unwrap CapabilityResult<T>
        │       ├── CapabilityResult.Success → return success.data()
        │       └── CapabilityResult.Failure → throw RuntimeException(errorCode + message)

        └─── EXTERNAL_SERVICE path ────────────────────────────────────────


            4b. URL safety check
                └── UrlValidator.assertNotInternal(endpoint)
                    └── Blocks SSRF to internal addresses (localhost, 169.254.x.x, etc.)

            5b. HTTP POST via RestClient
                └── POST {serviceBaseUrl}/capabilities/{capabilityKey}
                └── Headers: X-Workspace-Id, X-Actor-Id
                └── Timeouts: connect=5s, read=10s
                └── Response body deserialized to responseType

 4. Clear WorkspaceContext (finally block — always runs)

Capability key convention

AnnotationValueResulting key
@CapabilityGroup(prefix = "crm.product") + @Capability(key = "search")crm.product.search
@CapabilityGroup(prefix = "crm.deal") + @Capability(key = "")blank keycrm.deal

Keys use singular domain names and dot separators. Slashes and underscores are not used.


6. Event Publish & Dispatch Flow

Entry point: POST /api/workspaces/{workspaceId}/events/publish
Implementation: EventPublishServiceImplInProcessEventGateway (+ external webhook)

POST /api/workspaces/{workspaceId}/events/publish
  { eventKey, payload, actor }


 1. Permission check
    └── ExtensionPermissionService.assertPermission(tenantId, callerExtensionId, "events.publish:{eventKey}")



 2. EventBusGateway.publish(event)
    └── Dispatched to all registered gateways


        ├─── IN_PROCESS dispatch ──────────────────────────────────────────
        │       │
        │       ▼
        │   3a. Lookup handlers: registry[event.eventKey]
        │       └── Returns empty → no-op (event silently ignored)

        │   4a. Filter by workspace
        │       └── enabledByWorkspace[extensionKey].contains(event.tenantId)
        │       └── Prevents cross-tenant event leakage

        │   5a. For each matching handler:
        │       ├── async=false → safeInvoke() inline (blocks publisher)
        │       └── async=true  → eventExecutor.execute(() → safeInvoke())
        │           └── Queue full (RejectedExecutionException)
        │               └── Increment droppedEventCount counter
        │               └── Write to ext_dead_letter_event (QUEUE_FULL reason)

        │   6a. safeInvoke():
        │       ├── WorkspaceContext.set(event.tenantId)
        │       ├── Resolve first argument:
        │       │   ├── PlatformEvent subtype → pass event as-is
        │       │   └── EventPayload subtype  → ObjectMapper.convertValue(event.payload, paramType)
        │       ├── method.invoke(handler, firstArg, capabilityContext)
        │       ├── On exception:
        │       │   ├── Log error with extension, method, eventKey, workspaceId
        │       │   └── Write to ext_dead_letter_event (exception message as reason)
        │       └── WorkspaceContext.clear() (finally)

        └─── EXTERNAL_SERVICE dispatch ────────────────────────────────────


            3b. Query ext_event_subscription for (workspaceId, eventKey)
                └── Filter: extension is INSTALLED + enabled

            4b. HTTP POST to {serviceBaseUrl}/events
                └── Payload: full PlatformEvent JSON

Handler registration lifecycle

install() / enable()         →  InProcessEventGateway.register(workspaceId, extensionKey)
uninstall() / disable()      →  InProcessEventGateway.deregister(workspaceId, extensionKey)

Registration happens after transaction commit via TransactionSynchronizationManager.afterCommit(). This prevents the in-memory state from diverging from the DB if the transaction rolls back.

The same PF4J plugin instance serves all workspaces. The enabledByWorkspace map gates dispatch per workspace — handler methods are only stored once in the registry regardless of how many workspaces have the plugin enabled.

Handler method signature rules

java
// Required: 2 parameters
void onEvent(PlatformEvent event, CapabilityContext ctx)       // raw event
void onEvent(MyPayload payload, CapabilityContext ctx)         // typed, deserialized from event.payload

// NOT valid:
void onEvent(PlatformEvent event)                              // missing CapabilityContext
void onEvent(String raw, CapabilityContext ctx)                // String is not PlatformEvent/EventPayload

Dead letter queue

Failed and dropped events land in ext_dead_letter_event:

ColumnValue
workspaceIdTenant where event occurred
extensionKeyHandler extension that failed
eventKeyThe event that triggered the failure
reasonException message or "QUEUE_FULL"

7. Workspace Creation Flow

Entry point: POST /api/admin/workspaces
Implementation: WorkspaceServiceImpl.createWorkspace()

 1. Key uniqueness check
    └── workspaceRepository.existsByKey(key)
    └── throws IllegalArgumentException on duplicate

 2. Persist workspace metadata
    └── INSERT INTO workspace (key, name, ...)
    └── Returns entity with generated UUID id

 3. Create physical workspace database
    └── WorkspaceDatabaseInitializer.initializeDatabase(key)
        └── Creates a separate database/schema for this workspace
        └── [Implementation varies by database strategy]

 4. Pre-warm DataSource connection pool
    └── WorkspaceDataSourceRegistry.getOrCreate(workspaceId, workspaceKey)
        └── Makes the workspace DB immediately available for subsequent queries

8. User Registration Flow

Entry point: POST /api/auth/signup
Implementation: UserServiceImpl.register()

 1. Email validation (isEmailAllowed):
    ├── Check blacklist setting (SettingKey.BLACKLIST_EMAILS)
    │   └── Pipe-separated list; match = reject
    ├── Exact case-insensitive duplicate check
    │   └── userRepository.existsByEmailIgnoreCase(email)
    ├── Dot-sensitivity check (Gmail / edu.vn domains)
    │   └── Normalizes email: removes dots and +alias suffix
    │   └── Compares normalized form against all existing emails
    │   └── Prevents alias abuse (e.g., u.ser+tag@gmail.com ≡ user@gmail.com)
    └── Whitelist enforcement (SettingKey.WHITELIST_EMAILS)
        └── If set: email must contain one of the allowed patterns

 2. Create WorkspaceUser entity
    ├── password = BCrypt(password)
    ├── provider = LOCAL
    └── user.isNew = true (triggers referral tracking)

 3. Save user (INSERT)

 4. Referral tracking (if referral cookies present)
    ├── COOKIE_REFERRAL_CODE → user.referralUserId
    ├── COOKIE_REFERRAL_SITE → user.referralSite
    └── Delete both cookies from response

9. Authentication Flow

Entry point: POST /api/auth/login

 1. Spring Security validates credentials
    └── UserDetailsService.loadUserByUsername(email)
        └── Loads WorkspaceUser by email
        └── Maps PlatformRole.PLATFORM_ADMIN → ROLE_ADMIN + ROLE_USER
        └── All other roles → ROLE_USER

 2. JWT issued on success
    └── Contains: sub (email), roles, iat, exp

 3. Subsequent requests
    └── JWT validated by Spring Security filter chain
    └── Workspace-level access: @wsAccess.isMember(workspaceId, authentication)
    └── Admin-only routes: @PreAuthorize("hasRole('ADMIN')")

10. UI Mount Resolution Flow

Entry point: GET /api/catalog/workspaces/{workspaceId}/ui-mounts
Implementation: ExtensionUiResolver → provider chain

GET /api/catalog/workspaces/{workspaceId}/ui-mounts


 1. NavMountProviderImpl.getNavItems(workspaceId)
    └── UiMountRegistryService query: type = "nav" for workspaceId
    └── Returns: [ { label, icon, route, order } ]

 2. RouteMountProviderImpl.getRoutes(workspaceId)
    └── UiMountRegistryService query: type = "route" for workspaceId
    └── Returns: [ { path, componentRef, extensionKey } ]

 3. WidgetMountProviderImpl.getWidgets(workspaceId)
    └── UiMountRegistryService query: type = "widget" for workspaceId
    └── Returns: [ { slot, componentRef, extensionKey } ]

 4. Assemble UiMountBundleDTO { nav, routes, widgets }
    └── Response returned to frontend shell

The frontend shell calls this endpoint at login and uses the bundle to dynamically build its navigation tree, client-side routing table, and widget slots — with no hardcoded knowledge of which extensions are installed.


11. Cross-Extension Notification Pattern

Any extension can send a message to a chat channel without depending on the chat extension directly. The publisher emits a platform.notification event; the chat extension's NotificationEventListener handles it.

CRM Extension                    Platform                    Chat Extension
     │                              │                              │
     │  EventBusGateway.publish()   │                              │
     │──────────────────────────────▶                              │
     │   eventKey: "platform.notification"                         │
     │   payload:                   │                              │
     │     channel: "deals"         │                              │
     │     message: "New deal: ..." │                              │
     │     sender: "crm-bot"        │                              │
     │                              │                              │
     │                              │  InProcessEventGateway       │
     │                              │  dispatch to handlers        │
     │                              │──────────────────────────────▶
     │                              │                    NotificationEventListener
     │                              │                    @EventHandler(eventKey = "platform.notification")
     │                              │                    routes to channel "deals"

Publisher contract:

java
var event = PlatformEvent.builder()
    .eventKey("platform.notification")
    .tenantId(workspaceId)
    .actor("crm-bot")
    .payload(Map.of(
        "channel", "deals",
        "message", "Deal closed: Acme Corp — $50,000",
        "sender",  "CRM Bot"
    ))
    .build();

eventBusGateway.publish(event);

Well-known channel values: "deals", "general", or a conversation UUID.


12. Extension Config Flow

Use case: Extension requires API keys or settings before it can activate.

Install →  PENDING_CONFIG state (if any required field missing)


        PUT /api/catalog/workspaces/{workspaceId}/extensions/{extensionKey}/config
            { field: "apiKey", value: "sk-..." }


        ExtensionConfigService.saveConfig()
        └── Encrypts value via ConfigEncryptionService (AES)
        └── Writes to ext_extension_config



        POST /api/admin/workspaces/{workspaceId}/extensions/{extensionKey}/enable
        └── Enable flow re-validates all required fields
        └── All present → status = INSTALLED, enabled = true
        └── Still missing → throws (stays PENDING_CONFIG)

Sensitive config values (marked secret: true in ConfigManifest) are encrypted at rest and never returned in plaintext via any API response.

MetaOne Platform Documentation