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 | @senlerio/api | 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.
- Embedded page: launch, session, and project API access.
- Actions through MCP: publishing methods and calling them through AI.
- Automation step: configurator and webhook execution.
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.
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.
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.