// One mutation, three plugins. Effect-mode so it can `yield*` the plugin // services the framework provides in the per-request stack: // // 1. BILLING — requireEntitlement check-and-decrements the tenant's // `projects` quota (free = 3). Over quota → typed // EntitlementExceeded, the row is never written. // 2. ANALYTICS — useAnalytics().track(...) records a `project_created` // event. Noop-safe with no sink configured (see app.config). // 3. NOTIFICATIONS — NotificationService.send(...) drops a note in the // owner's in-app inbox + the console channel. import { Effect } from 'effect' import type { AppContext } from '@voltro/runtime' import { EffectStore, useAnalytics } from '@voltro/runtime' import { requireEntitlement } from '@voltro/plugin-billing' import { NotificationService } from '@voltro/plugin-notifications' const execute = (input: { name: string }, ctx: AppContext) => Effect.gen(function* () { // 1. Billing gate — fails closed (typed EntitlementExceeded) when the // tenant has no `projects` quota left. Runs BEFORE the write. yield* requireEntitlement(ctx, 'projects', 1) // 2. Write the row. `tenant()` auto-stamps tenantId + audit columns. const store = yield* EffectStore const row = yield* store.insert('projects', { name: input.name }) // 3. Analytics — fire-and-forget event. const analytics = yield* useAnalytics() yield* analytics.track({ name: 'project_created', subjectId: ctx.request.subject.id, properties: { name: input.name }, }) // 4. Notify the owner (in-app inbox + console). const notify = yield* NotificationService yield* Effect.promise(() => notify.send({ to: ctx.request.subject.id ?? 'system', category: 'project', title: 'Project created', body: `"${input.name}" is live.`, })) return { id: row['id'] as string, name: row['name'] as string, tenantId: row['tenantId'] as string, } }) export default execute