RefLink: Plugin Example
RefLink is an open-source, full-featured referral campaign plugin for Senler. One repository contains the frontend, backend, shared contracts, migrations, user documentation, and self-hosting files.
This guide explains the application in the order in which real work happens: how the embedded page opens, how the backend gains access to a project, how AI calls actions through MCP, and how an automation step runs. You can then move on to the specific directories and files.
How To Use The Example
RefLink is useful when a new plugin needs several capabilities at once:
- an embedded page inside the cabinet;
- OAuth access to the Senler API for a project;
- a custom automation step and its configuration form;
- backend actions available to AI through MCP;
- webhooks with signature verification and duplicate-processing protection;
- shared types between the frontend and backend;
- public documentation in Russian and English.
Do not copy the entire application. Select the required flow first, then reuse the corresponding layer and adapt permissions, data, and session lifetimes to your product.
RefLink does not implement an agent tool configurator. That context follows the same Senler Bridge approach, but its backend operation is marked with AgentToolConfigurator and its frontend handles a tool_configurator launch.
Technologies And Their Roles
| Part | Technology | Purpose |
|---|---|---|
| Frontend | React and Senler UI | The embedded page interface, step configuration form, and cabinet communication through Senler Bridge. |
| Backend | NestJS | HTTP controllers, DTO validation, application modules, OAuth, sessions, and webhook handlers. |
| API contract | OpenAPI and NestJS Swagger | Regular endpoint descriptions and selected MCP application actions. |
| Senler API | @aisenler/sdk-fetch | Typed calls for channels, leads, variables, agents, segments, and automations. |
| Storage | PostgreSQL and TypeORM | Campaigns, OAuth connections, processed events, and technical reward-delivery state. |
| Local runtime | Docker Compose | Reproducible startup of the frontend, backend, and PostgreSQL. |
How A Request Flows
RefLink has three primary flows. They share one backend but use different entry points and permissions.
1. Opening The Embedded Page
- A user opens RefLink in a project. The cabinet loads the frontend as an embedded page and supplies launch context through Senler Bridge.
- The frontend receives
launch_codeandproject_idinuseEmbeddedRefLinkApp.ts. The interface usesproject_idto check consistency, but the value alone grants no access. - The frontend sends the one-time
launch_codetoPOST /api/embedded/session. - The backend verifies the code's signature, lifetime, and single use in
backend/src/core/session, extracts the project, and issues its own RefLinksession_token. - Every later page request uses this
session_token.EmbeddedSessionGuardextracts the project from the verified session again, so controllers do not need aproject_idfrom a form or URL. - If the project has not completed OAuth, the frontend displays
AuthorizationStep. After consent, the backend receives project-scoped Senler OAuth tokens and stores them for this project. - To load channels, agents, segments, and automations, the backend obtains an OAuth access token and calls the Senler API through
SenlerApiClient.
Result: the browser knows only the RefLink session, while Senler API access and the Client Secret remain on the backend.
2. Calling An Application Action Through MCP
- The developer marks permitted operations with SDK decorators. Read, create, update, and archive campaign examples are in
campaign.controller.ts. - NestJS Swagger generates OpenAPI. The
AppActiondecorator addsx-senler-app-actionto a selected operation; other endpoints do not become actions automatically. - After the plugin is installed, Senler loads OpenAPI from the URL configured for the application. AI discovers an action through
search, reads its complete schema throughdescribe_methodwhen needed, and calls it throughexecute. - Before calling the backend, Senler creates a short management session by sending a signed one-time
launch_codetoPOST /api/embedded/management-session. - RefLink verifies the code and returns its own
management_token. Only this token is sent inAuthorization: Bearerwhen the action is called. The user's MCP key or OAuth token is never forwarded to the plugin. - The same
EmbeddedSessionGuardverifies the management token and passes the confirmed project to the controller. Therefore, an application action does not acceptproject_idfrom AI. - If the action also needs Senler data, the backend separately uses the stored project-scoped OAuth access token.
A separate configurator example is in app-action-configurator.controller.ts. The AutomationStepConfigurator decorator tells MCP that the method result contains normalized step configuration and branches.
3. Configuring And Running An Automation Step
- When a user adds a RefLink step in the editor, Senler opens the same frontend with an
automation_step_configuratorlaunch type. useEmbeddedRefLinkApp.tsreads the launch type, current configuration, and branches from Senler Bridge.- Instead of the main page,
App.tsxdisplaysStepConfigurator. - On save,
configurator-protocol.tsreturns normalized configuration and automation-result variable bindings through the Bridge. - During a published run, Senler does not open the form again. It calls the signed step webhook in
webhook.controller.ts. - The backend verifies the signature, runs the use case, and returns the fields defined in the shared
REFLINK_STEP_RESULTScontract.
Result: the configurator only prepares and saves settings, while the webhook executes the step during an automation run.
Recommended Source Reading Order
| Step | Repository section | What it explains |
|---|---|---|
| 1 | README.md | Application purpose, campaign modes, startup, and documentation publication. |
| 2 | docs/architecture.md | Where data lives and why referral statistics are not duplicated in PostgreSQL. |
| 3 | docs/senler-app-setup.md | Which URLs, events, OAuth permissions, and result fields belong in application settings. |
| 4 | contracts/src/index.ts | Shared types, campaign modes, and step results used by both sides of the application. |
| 5 | frontend/src | Bridge connection, code-to-session exchange, and switching between the main page and configurator. |
| 6 | backend/src/core/session | One-time code verification, project-scoped sessions, and endpoint protection. |
| 7 | backend/src/resources/oauth | OAuth callback, connection storage, and token refresh. |
| 8 | backend/src/integrations/senler | The isolated Senler API adapter. |
| 9 | backend/src/resources/campaigns | Campaign CRUD, statistics, automation creation, and OpenAPI actions. |
| 10 | backend/src/resources/referrals | Webhook signatures, deduplication, attribution, and reward delivery. |
| 11 | docs/public and assets/catalog | Keeping user documentation and application catalog assets beside the code. |
| 12 | docker-compose.yml | How the frontend, backend, database, health check, and environment variables fit together. |
Key Architectural Decisions
The Project Comes From Trusted Context
A project_id in a query parameter, request body, or AI argument is not proof of access. RefLink determines the project only after verifying a launch_code, application session, management token, OAuth subject, or webhook signature. Services then receive the confirmed ID through @EmbeddedProject().
This prevents a user or AI from substituting another project's ID in an ordinary parameter.
Authorization Is Split By Purpose
| Value | Issuer | Purpose |
|---|---|---|
launch_code | Senler | Confirms a page or management-session launch once and carries signed project context. |
session_token | RefLink backend | Lets the opened frontend call the RefLink backend within one project. |
management_token | RefLink backend | Lets Senler call a particular application action through MCP. |
| OAuth access/refresh token | Senler OAuth | Lets the RefLink backend call the Senler API with user-approved project permissions. |
Never use one of these tokens in place of another. OAuth secrets are encrypted through SecretVaultService, and the new access/refresh pair is persisted together while holding a connection lock in OAuthConnectionService.
One Frontend Serves Multiple Contexts
A shared entry point avoids duplicating authorization, data loading, styles, and components. The difference between the main page and step form comes from the verified context.launch.type supplied by Senler Bridge, not a separate URL.
If the application adds an agent tool configurator, this design can grow by adding an explicit tool_configurator branch without mixing configuration persistence with normal page behavior.
The SDK Is Separated From Domain Logic
Campaign controllers and services do not construct Senler HTTP requests directly. All such calls are collected in SenlerApiClient, which uses the official SDK.
This makes SDK upgrades and response testing easier and provides one place to isolate operations that are not yet available in a published client version.
Webhook Retries Are Expected
A webhook may arrive more than once, or a handler may stop after partial work. WebhookReceiptRepository uses the event ID for deduplication, stores processing status, and allows stalled work to be reclaimed after a bounded lease.
Domain operations are also atomic or idempotent: the first inviter is written only once, and an invited lead ID is added to an array without duplicates.
Data Ownership Is Chosen Up Front
RefLink stores campaign settings, OAuth connections, and technical state in PostgreSQL. Referral relationships and counters remain in Senler lead variables. The application does not maintain a second copy of the same statistics or synchronize two sources of truth.
This is not a universal data model, but it demonstrates a universal principle: decide who owns each data type before implementation and store locally only the data that belongs to the application.
What To Reuse In A New Application
- Create separate packages or directories for shared contracts, frontend, backend, and the integration adapter.
- Implement
launch_codeverification and a project-scoped session before building business forms. - Add OAuth with only the required permissions, encrypt tokens, and persist the rotated token pair atomically after refresh.
- Connect Senler UI and Senler Bridge, then handle every required
context.launch.typeexplicitly. - Keep business endpoints as regular DTO-based controllers, then publish selected operations with
AppAction,AgentToolConfigurator, orAutomationStepConfigurator. - Validate generated OpenAPI with
senler-app validate-openapibefore saving its URL in application settings. - For webhooks, implement signature verification, event IDs, idempotency, and stalled-work recovery before adding the domain use case.
- Keep user documentation and catalog assets beside the source and check the Russian and English versions in CI.
What Not To Copy Without Review
- RefLink OAuth permissions: a new application may require fewer or different permissions.
- Session lifetime: choose it according to risk, relaunch behavior, and product requirements.
- Lead variable names, webhook events, result fields, and reward rules: they belong to the referral application's domain model.
- Endpoint URLs and environment variable names: they must belong to your domain and infrastructure.
- Every MCP action at once: publish only operations that AI must call, and set
read_only,destructive, andidempotentaccurately.