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:
extensionRuntimeService.undeploy()is called to remove the ghost pluginext_installation.statusis set toFAILEDwith the error message- An
FAILUREaudit log entry is written - The exception is swallowed — the method returns the
FAILEDinstallation 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 log3. Extension Enable / Disable Flow
Entry point:
POST /api/admin/workspaces/{workspaceId}/extensions/{extensionKey}/enablePOST /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 logDisable
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 log4. 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()andinstall()share one Spring transaction (defaultREQUIREDpropagation). Ifinstall()were changed toREQUIRES_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: CapabilityInvocationServiceImpl → CapabilityGatewayFactory
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
| Annotation | Value | Resulting key |
|---|---|---|
@CapabilityGroup(prefix = "crm.product") + @Capability(key = "search") | — | crm.product.search |
@CapabilityGroup(prefix = "crm.deal") + @Capability(key = "") | blank key | crm.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: EventPublishServiceImpl → InProcessEventGateway (+ 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 JSONHandler 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
// 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/EventPayloadDead letter queue
Failed and dropped events land in ext_dead_letter_event:
| Column | Value |
|---|---|
workspaceId | Tenant where event occurred |
extensionKey | Handler extension that failed |
eventKey | The event that triggered the failure |
reason | Exception 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 queries8. 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 response9. 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 shellThe 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:
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.