Building an app for Effektivflyt
An app is a small web page from another system, shown inside Effektivflyt in one or both of two places:
- In each case: a box in the case's side panel. It knows the open case and, if the company allows it, can add notes and tags (a customer card from an ERP, tracking for a shipment).
- In the left sidebar: a row under Views that opens the app as a full page, with no case (a customer list, a dashboard, a stock lookup).
┌ sidebar ────────┐┌ main area ──────────────────────────┐┌ case panel ─────────┐
│ Team inbox ││ the case's conversation ││ Requester, … │
│ … ││ ││ ┌ Customer card ──┐ │
│ Views ││ (or, from the sidebar row, ││ │ your page │ │
│ Customer card ←││ your page, full size) ││ └─────────────────┘ │
└─────────────────┘└─────────────────────────────────────┘└─────────────────────┘
There is no review and no SDK to install: you put two files on any web server, and a company
admin installs your app by its address. To build and try it, get a free developer company at
/developers (sign in, one click): a company of your own on invented customers, orders and
PDFs, where you are the admin. It can't connect real mailboxes.
Quick start on your computer
In a terminal (with Effektivflyt running at http://localhost:5173; the /developers page shows
these lines with its own address):
mkdir my-app && cd my-app
curl -fO http://localhost:5173/developers/starter/index.html -fO http://localhost:5173/developers/starter/app.js -fO http://localhost:5173/developers/starter/effektivflyt-app.json
python3 -m http.server 3000
The downloaded index.html already loads the kit and styles from that Effektivflyt. Open
http://localhost:3000 for a preview: opened on its own, the kit gives the page a sample case
and answers every call with sample data (nothing is saved; a blue bar says so; add
?location=sidebar for the sidebar page). Edit app.js (inCase draws the box in a case,
inSidebar the sidebar page) and reload. To see it in real cases, install
http://localhost:3000/ in your developer company (below).
1. Two files on your server
effektivflyt-app.json, the description admins see before they install:
{
"name": "Customer card",
"description": "The requester's account from our ERP: balance, terms and last orders.",
"url": "./index.html",
"locations": ["case", "sidebar"],
"icon": "contact",
"permissions": ["case.read", "case.notes.write"],
"height": 300,
"author": { "name": "Acme Integrations" }
}
| Field | ||
|---|---|---|
name |
required | at most 60 characters, shown as the box's heading |
url |
optional | the page to show, relative to the file (default ./) |
locations |
optional | case (a box in each case, the default), sidebar (its own page from the left sidebar), or both |
icon |
optional | the sidebar row's icon: boxes (default), briefcase, building, calendar-days, chart-column, chart-line, clipboard-list, contact, file-spreadsheet, gauge, kanban, layout-dashboard, list, receipt, shopping-cart, store, workflow |
permissions |
optional | what the app may do, see below. Ask for as little as you need. |
height |
optional | starting height in pixels, 80–800 (the kit adjusts it to your content) |
description, author |
optional | shown to the admin when installing |
The page must be on https:// (http://localhost works while you try it out).
Your page loads the kit (and, to look at home, the styles) from the Effektivflyt address the company uses:
<link rel="stylesheet" href="https://app.effektivflyt.no/apps/ui.css">
<script src="https://app.effektivflyt.no/apps/sdk.js"></script>
<script>
Effektivflyt.ready().then(context => {
document.body.textContent = `Case from ${context.requester.name} <${context.requester.email}>`;
});
</script>
2. Install it
Settings → Admin → Apps → paste the address of the folder (or of the .json file) →
Look at it. The admin sees your app's name, maker, address and what it asks for, then clicks
Install. Admins can switch an app off or remove it at any time. There is a ready example,
Customer card, under Install an app: its source is in static/apps/examples/customer-card/.
Publishing it in the app directory
Settings → Apps has an app directory: apps the owner has looked at, which any company's admin
installs in one click (they still see what it may do first). To get yours in, open
/developers/publish, give the address of your app (where its effektivflyt-app.json is) and a
category, and Submit for review. You see the answer there, with any note.
- The directory lists apps on
https://only (on a--testserver,localhostis allowed too). - Changed your app? Read it again. New text keeps it listed; asking for a new permission or a new place sends it back to review. An admin installing it is warned if it asks for more than what was reviewed.
- Take it out removes it from the directory; companies that installed it keep it.
3. The kit (/apps/sdk.js)
| Call | Permission | What it does |
|---|---|---|
await Effektivflyt.ready() |
none | the context (below); call it first. Outside Effektivflyt: a sample (context.preview is true) |
Effektivflyt.on('case.changed', fn) |
none | fn(context) when the case changes (reply, note, tag, status) |
await Effektivflyt.case.get() |
case.read |
{ status, assignee, urgent, tags, notes } (notes is a count) |
await Effektivflyt.tags.list() |
case.read |
the company's tag names (also on a sidebar page) |
await Effektivflyt.notes.add(text) |
case.notes.write |
an internal note; the history shows Anna Berg via Customer card |
await Effektivflyt.tags.add(name), .remove(name) |
case.tags.write |
by tag name |
await Effektivflyt.token() |
none | a signed token for your own server (below; also on a sidebar page) |
Effektivflyt.openLink(url) |
none | opens a web address in a new tab |
Effektivflyt.resize(px?) |
none | sets the box's height; done for you as your page grows |
A refused or failed call rejects with an Error whose code is e.g. permission_denied,
tag_unknown, note_invalid, app_disabled or timeout.
Calls about the case work only in a case; on a sidebar page they reject with case_missing.
The context:
{
"location": "case",
"case": { "id": "AAMk…", "mailbox": "post@company.dk", "subject": "Order 4471", "conversationId": "…", "receivedAt": "…", "emailIds": ["…"] },
"requester": { "name": "Lars Mikkelsen", "email": "lars.mikkelsen@nordiskbyg.dk" },
"user": { "name": "Mette Jensen" },
"company": { "name": "Nordisk Grossist A/S" },
"permissions": ["case.read", "case.notes.write"],
"locale": "da",
"theme": { "mode": "dark", "colors": { "accent": "#7B93F5", "text": "#ECEEF2", "…": "…" } },
"revision": 3
}
On a sidebar page location is "sidebar" and case and requester are null. Each case opens
your page fresh; case.changed is for changes while it is open.
Design and code: making it fit, and making it yours
Start from the starter (/apps/starter/ on any Effektivflyt address, source in
web/static/apps/starter/): a description file, a page and a script that handle both places.
Customer card (/apps/examples/customer-card/) is a fuller example.
Look. /apps/ui.css gives you Effektivflyt's own look: text (ef-title, ef-heading,
ef-muted), layout (ef-stack, ef-row, ef-spread, ef-card), facts (ef-facts), badges
(ef-badge accent|success|danger), buttons (ef-button primary|danger|link), fields (ef-label,
ef-input) and tables (ef-table, ef-end). Open /apps/ui.html to see every piece. The kit
fills the --ef-* colours with the ones the person is looking at (light, dark, or another
theme), so your app changes with them. Outside Effektivflyt the page follows the computer's dark
mode, so you can build it in a normal browser tab.
Make it yours. Everything in ui.css is a default: set any colour in your own CSS (they win
over Effektivflyt's), e.g. :root { --ef-accent: #0f766e; } for your brand colour, or skip
ui.css and style it all yourself. Keep it readable in dark mode ([data-ef-theme="dark"]) and
narrow (the case panel is about 320 px wide); a sidebar page has the whole main area
([data-ef-location="sidebar"]).
Good manners.
- Show something useful at once; load the rest after. Say plainly when there is nothing to show.
- Put buttons at the bottom of the box, in the person's words ("Note the account"), and say
what happened afterwards (
ef-status). - Don't repeat what Effektivflyt already shows (subject, requester, status).
- Your page opens again for every case; keep it light (no big frameworks needed).
- Never ask the person to sign in inside the box; use
Effektivflyt.token()and your own server.
4. Trusting requests on your server
What your page tells your server could be made up by anyone. To know a request really comes from someone looking at a case in Effektivflyt, send the token along and check it:
const token = await Effektivflyt.token();
const answer = await fetch('/customer', { headers: { Authorization: `Bearer ${token}` } });
The token is a JWT (HS256) signed with the app's signing secret, which the admin sees once
after installing and gives you. It lasts five minutes. Claims: iss (effektivflyt), aud (the
app's id in that company), sub (the person's email), name, company, companyName,
mailbox, caseId, iat, exp.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(token, secret) {
const [header, body, signature] = token.split('.');
const expected = createHmac('sha256', secret).update(`${header}.${body}`).digest('base64url');
if (!signature || !timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) return null;
const claims = JSON.parse(Buffer.from(body, 'base64url').toString());
return claims.exp * 1000 > Date.now() ? claims : null;
}
Any JWT library works too (jsonwebtoken, jose, PyJWT with algorithms=["HS256"]).
What an app can't do
- Read the conversation's text, attachments or other cases; send mail; change status or
assignee. (The Cases API,
api guide, is for systems that need more.) - Reach Effektivflyt directly: its frame has no access to the page or its cookies, and every request goes through the kit and is checked on the server against both the app's approved permissions and the person's own access to the mailbox.
- Show itself where it wasn't installed: the page's
frame-srclists only switched-on apps. - Put rows anywhere but the sidebar's Views, or pick an icon outside the list above.
Inside
src/lib/server/apps/ (the installed_apps table, reading descriptions, the actions and their
permissions, tokens), routes/api/apps/ (install, switch, remove, actions) and
src/lib/cases/plugins/apps/ (Settings → Apps, the case-panel box, the sidebar page
AppScreen.svelte through the plugin's dynamicViews, and the message bridge bridge.ts). The
kit and styles are web/static/apps/sdk.js and ui.css. hooks/securityHeaders.ts adds switched-on apps' origins to frame-src.