Introduction
Support Frix is a framework-agnostic support widget. You embed a single custom element, and your users get a polished button that opens a modal where they pick a category and send a message. Tickets are saved to your database and mirrored to a Telegram channel, and your replies flow back into the widget in real time.
The widget renders inside the Shadow DOM, so it is fully isolated: your CSS can't break it and its CSS can't leak into your app.
Installation
Load the bundle once, then place the element anywhere in your markup.
<script type="module"
src="https://support.frix.me/widget/support-frix-widget.js"></script>
<support-frix-widget
api-key="YOUR_API_KEY"
auth-mode="both"
></support-frix-widget>Get YOUR_API_KEY from the dashboard → Platforms. Self-hosting? Point the script at your own /widget/ path and set api-url accordingly.
Configuration
Everything is a plain HTML attribute. No bundler, no config files.
| Attribute | Default | Notes |
|---|---|---|
| api-key | — | Required. Platform key from the dashboard. |
| api-url | https://support.frix.me/api | Base URL of the API. |
| auth-mode | both | email · telegram · both |
| primary-color | #6366f1 | Accent color. |
| btn-radius | 9999px | Launcher corner radius. |
| btn-position | bottom-right | bottom-right · bottom-left |
| btn-label | Support | Launcher button text, and its accessible name. |
| show-label | true | false hides the text, leaving an icon-only circle. |
| btn-icon | chat | chat · bubbles · help · headset · sparkle · envelope · lifebuoy · none |
| btn-emoji | — | Any emoji to use as the glyph. Overrides btn-icon. |
| btn-image | — | Image URL for the glyph. Overrides btn-emoji. |
| btn-size | md | sm · md · lg |
| title | How can we help? | Modal heading. |
| theme | light | light · dark · auto |
| skin | flat | flat · glass · neumorphism · clay |
Identity modes
The auth-mode attribute decides how a user identifies before sending a ticket.
email— the user leaves a name and email. Lowest friction; ideal for public marketing sites.telegram— the user enters their Telegram, receives a 6-digit code from the bot, and confirms. Replies then reach them directly on Telegram.both— show both options and let the user pick. A sensible default.
Receiving replies
However a user identified, your answer reaches them:
- Inside the widget. On submit, the widget stores a private
publicTokeninlocalStorageand polls/api/tickets/:token. When you reply, the conversation updates automatically — no login required. - On Telegram. If the user verified via Telegram, the bot also DMs them your answer with the platform name as a header.
Framework guides
Because it's a standard web component, integration is nearly identical everywhere — only the "allow this custom tag" detail differs.
// React treats unknown tags as custom elements automatically.
import { useEffect } from "react";
export function SupportButton() {
useEffect(() => {
const s = document.createElement("script");
s.type = "module";
s.src = "https://support.frix.me/widget/support-frix-widget.js";
document.body.appendChild(s);
}, []);
return (
// @ts-expect-error — custom element has no JSX types
<support-frix-widget api-key="YOUR_API_KEY" auth-mode="both" />
);
}Styling & theming
Match your brand with primary-color, btn-radius, btn-position and theme. Because the widget lives in the Shadow DOM, these attributes are the supported styling surface — your global stylesheet won't reach inside.
<support-frix-widget
api-key="YOUR_API_KEY"
primary-color="#0ea5e9"
btn-radius="14px"
btn-position="bottom-left"
btn-label="Help"
theme="dark"
></support-frix-widget>Skins
theme picks the colours; skin picks the material. They are independent — every skin has a light and a dark form.
flat— the default. Solid surfaces, one soft shadow.glass— translucent, blurred panels that pick up the page behind them. Best over photography or a colourful background.neumorphism— one continuous surface lit from the top left, with pressed-in fields and raised controls. It sets its own background tone, so it reads best on a plain page.clay— puffed, heavily rounded shapes with a soft inner light.
<support-frix-widget
api-key="YOUR_API_KEY"
skin="glass"
theme="auto"
></support-frix-widget>The launcher button
The floating button takes a title, a glyph, and a size. Drop show-label to false (or set an empty btn-label) for an icon-only circle. The glyph resolves most-specific-first: btn-image beats btn-emoji, which beats btn-icon.
<!-- Icon-only, with your own artwork -->
<support-frix-widget
api-key="YOUR_API_KEY"
show-label="false"
btn-image="/logo-mark.svg"
btn-size="lg"
></support-frix-widget>
<!-- A built-in icon, and an emoji instead of one -->
<support-frix-widget api-key="..." btn-icon="lifebuoy"></support-frix-widget>
<support-frix-widget api-key="..." btn-emoji="👋"></support-frix-widget>prefers-reduced-motion, so visitors who ask for less movement get none.Create the bot
You only do this once for your whole installation.
- Open @BotFather in Telegram and send
/newbot. - Choose a name and a username (e.g.
SuppFrixBot). - Copy the token BotFather gives you and set it as
TELEGRAM_BOT_TOKENin your API.env.
Create the channel & add the bot
This is the channel every ticket is mirrored into. Do it from your own Telegram account — a bot can't create channels.
- In Telegram: New Channel → name it (e.g. "Frix Support"). Private is fine.
- Open the channel → Administrators → Add Admin.
- Search
@SuppFrixBotand add it. Grant at least Post Messages. - In Dashboard → Platforms, click Connect a Telegram channel and post the shown code (e.g.
SF-AB23CD) in the channel. The bot reads it, links the channel automatically, and the dashboard flips to "Connected" on its own — no manual channel-ID lookup needed.
Wire it to the API
Add the channel ID to your environment and restart the API.
# apps/api/.env
TELEGRAM_BOT_TOKEN="123456:ABC-yourtoken"
TELEGRAM_BOT_USERNAME="SuppFrixBot"
TELEGRAM_CHANNEL_ID="-1001234567890"
# production webhook (https, public)
TELEGRAM_WEBHOOK_URL="https://support.frix.me/api/telegram/webhook"In development the bot uses long-polling automatically. In production it registers the webhook above so Telegram delivers owner replies to the API.
Using the dashboard
- Platforms — create one per app you embed in. Each gets its own API key and categories.
- Categories — the options users pick from when writing in.
- Tickets — a master-detail inbox. Pick a ticket, read the thread, and reply from the field at the bottom; the answer is pushed to the user. Tick several at once — hover a row, or use the select button — to move them all to a status, or delete them.
- Admins — give a teammate access to specific platforms. Each grant carries its own permissions: answering tickets, editing settings, and deleting conversations. Deleting is off by default and is the only one that destroys history, so it is always handed over deliberately.
- Telegram toggle — turn channel/DM notifications on or off per platform.
API reference
The widget only touches the public endpoints; the rest power the dashboard and bot.
| GET | /api/platforms/categories?apiKey=… | Public. Categories for a platform — used by the widget to populate the dropdown. |
| POST | /api/tickets/create | Public. Create a ticket. Returns a publicToken the widget keeps to poll for replies. |
| GET | /api/tickets/:publicToken | Public. Ticket + full message thread, by token. Powers the widget conversation view. |
| POST | /api/tickets/:publicToken/messages | Public. Append a follow-up message from the user. |
| POST | /api/tickets/:publicToken/rate | Public. Submit a 1-5 satisfaction rating. Only accepted once the ticket is CLOSED. |
| POST | /api/auth/request-code | Send a 6-digit Telegram verification code to a telegramId. |
| POST | /api/auth/verify-code | Verify the code, mark the user verified, return a short-lived token. |
| POST | /api/auth/login | Owner login for the dashboard. Returns a JWT. |
| GET | /api/owner/tickets | Auth. List tickets across the owner's platforms, filterable by status/platform. |
| POST | /api/owner/tickets/:id/reply | Auth. Post an answer. Delivered to the user via the widget and Telegram. |
| PATCH | /api/owner/tickets/:id/status | Auth. Move one conversation to OPEN, IN_PROGRESS or CLOSED. |
| DELETE | /api/owner/tickets/:id | Auth. Permanently delete a conversation. Requires the platform owner, or an admin granted “delete conversations”. |
| POST | /api/owner/tickets/bulk/status | Auth. Move a list of ticketIds to one status. Fails whole if any of them is read-only for you. |
| POST | /api/owner/tickets/bulk/delete | Auth. Permanently delete a list of ticketIds. Fails whole if any of them is one you may not delete. |
| POST | /api/telegram/webhook | Telegram → API. Handles owner replies coming from the channel/DM. |
Self-hosting
Support Frix is a small monorepo: a NestJS API, a Next.js dashboard, and the Vite widget.
# build everything
npm install
npm run build # api + dashboard + widget
# database
npm run db:up # postgres via docker
npm run db:deploy -w api
# run with pm2 (api :3000, dashboard :3001)
pm2 start ecosystem.config.jsnginx serves the built widget from /widget/, proxies /api/ to the API and everything else to the dashboard. The full nginx block ships in SETUP.md.
Ready to embed it? Grab the snippet for your framework →