ruLog in to Senler

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

PartTechnologyPurpose
FrontendReact and Senler UIThe embedded page interface, step configuration form, and cabinet communication through Senler Bridge.
BackendNestJSHTTP controllers, DTO validation, application modules, OAuth, sessions, and webhook handlers.
API contractOpenAPI and NestJS SwaggerRegular endpoint descriptions and selected MCP application actions.
Senler API@aisenler/sdk-fetchTyped calls for channels, leads, variables, agents, segments, and automations.
StoragePostgreSQL and TypeORMCampaigns, OAuth connections, processed events, and technical reward-delivery state.
Local runtimeDocker ComposeReproducible 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

  1. A user opens RefLink in a project. The cabinet loads the frontend as an embedded page and supplies launch context through Senler Bridge.
  2. The frontend receives launch_code and project_id in useEmbeddedRefLinkApp.ts. The interface uses project_id to check consistency, but the value alone grants no access.
  3. The frontend sends the one-time launch_code to POST /api/embedded/session.
  4. The backend verifies the code's signature, lifetime, and single use in backend/src/core/session, extracts the project, and issues its own RefLink session_token.
  5. Every later page request uses this session_token. EmbeddedSessionGuard extracts the project from the verified session again, so controllers do not need a project_id from a form or URL.
  6. 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.
  7. 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

  1. The developer marks permitted operations with SDK decorators. Read, create, update, and archive campaign examples are in campaign.controller.ts.
  2. NestJS Swagger generates OpenAPI. The AppAction decorator adds x-senler-app-action to a selected operation; other endpoints do not become actions automatically.
  3. 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 through describe_method when needed, and calls it through execute.
  4. Before calling the backend, Senler creates a short management session by sending a signed one-time launch_code to POST /api/embedded/management-session.
  5. RefLink verifies the code and returns its own management_token. Only this token is sent in Authorization: Bearer when the action is called. The user's MCP key or OAuth token is never forwarded to the plugin.
  6. The same EmbeddedSessionGuard verifies the management token and passes the confirmed project to the controller. Therefore, an application action does not accept project_id from AI.
  7. 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

  1. When a user adds a RefLink step in the editor, Senler opens the same frontend with an automation_step_configurator launch type.
  2. useEmbeddedRefLinkApp.ts reads the launch type, current configuration, and branches from Senler Bridge.
  3. Instead of the main page, App.tsx displays StepConfigurator.
  4. On save, configurator-protocol.ts returns normalized configuration and automation-result variable bindings through the Bridge.
  5. During a published run, Senler does not open the form again. It calls the signed step webhook in webhook.controller.ts.
  6. The backend verifies the signature, runs the use case, and returns the fields defined in the shared REFLINK_STEP_RESULTS contract.

Result: the configurator only prepares and saves settings, while the webhook executes the step during an automation run.

StepRepository sectionWhat it explains
1README.mdApplication purpose, campaign modes, startup, and documentation publication.
2docs/architecture.mdWhere data lives and why referral statistics are not duplicated in PostgreSQL.
3docs/senler-app-setup.mdWhich URLs, events, OAuth permissions, and result fields belong in application settings.
4contracts/src/index.tsShared types, campaign modes, and step results used by both sides of the application.
5frontend/srcBridge connection, code-to-session exchange, and switching between the main page and configurator.
6backend/src/core/sessionOne-time code verification, project-scoped sessions, and endpoint protection.
7backend/src/resources/oauthOAuth callback, connection storage, and token refresh.
8backend/src/integrations/senlerThe isolated Senler API adapter.
9backend/src/resources/campaignsCampaign CRUD, statistics, automation creation, and OpenAPI actions.
10backend/src/resources/referralsWebhook signatures, deduplication, attribution, and reward delivery.
11docs/public and assets/catalogKeeping user documentation and application catalog assets beside the code.
12docker-compose.ymlHow 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

ValueIssuerPurpose
launch_codeSenlerConfirms a page or management-session launch once and carries signed project context.
session_tokenRefLink backendLets the opened frontend call the RefLink backend within one project.
management_tokenRefLink backendLets Senler call a particular application action through MCP.
OAuth access/refresh tokenSenler OAuthLets 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

  1. Create separate packages or directories for shared contracts, frontend, backend, and the integration adapter.
  2. Implement launch_code verification and a project-scoped session before building business forms.
  3. Add OAuth with only the required permissions, encrypt tokens, and persist the rotated token pair atomically after refresh.
  4. Connect Senler UI and Senler Bridge, then handle every required context.launch.type explicitly.
  5. Keep business endpoints as regular DTO-based controllers, then publish selected operations with AppAction, AgentToolConfigurator, or AutomationStepConfigurator.
  6. Validate generated OpenAPI with senler-app validate-openapi before saving its URL in application settings.
  7. For webhooks, implement signature verification, event IDs, idempotency, and stalled-work recovery before adding the domain use case.
  8. 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, and idempotent accurately.