Modules
Privacy friendly web analytics that lives inside your Elixir app: the same
library as @runlight/sdk, the same tables, and the same answers, so an
Elixir app and a Node or PHP one can share a database.
A problem with an account change, to show the person making it, with a code and params.
Accounts: who may sign in, their password hashes, two-factor, invites, and
the signed cookie that keeps them signed in (the SDK's accounts/auth.ts).
An app turns them on with Runlight.routes(rl, accounts: true).
Accounts on the web: sign-in, the code step, invites, first-run setup, and
the Account and People APIs, under the base path the routes answer at (the
SDK's accounts/web.ts). Runlight.routes(rl, accounts: true) makes one.
What went wrong with the assistant, as a code the dashboard says in its own words.
Why connecting another install failed, as a code the dashboard says in its own words.
The little the store needs from a database: its dialect, and statements
with ? placeholders that answer rows. Runlight.Db.Ecto runs them through
an app's own Ecto repo; anything else that implements the callbacks below
can stand in.
Runlight's statements through an app's own Ecto repo, on SQLite
(ecto_sqlite3), Postgres (postgrex), or MySQL 8.4 and MariaDB 11.4
and later (myxql). The repo's adapter picks the dialect.
Outgoing requests, the stand-in for JavaScript's fetch(). Everything that calls another server (mail services, importers, connected installs, the assistant's providers, site icons) goes through the instance's fetcher, so tests can pass a fake.
Why a funnel was refused, as a code the dashboard says in its own words.
Where a visitor is, from a hosting platform's headers or a database lookup.
Why a goal was refused, as a code the dashboard says in its own words.
A request's or a response's headers: a list of {name, value} pairs with
lowercase names, read and set as the Fetch API's Headers are.
A request, shaped like the Fetch API's Request so the routes read as the
TypeScript SDK's do: an absolute URL, a method, headers, and the body as
text. Runlight.Plug makes one from a Plug.Conn; a test or another
server can make one with new/2.
An answer, shaped like the Fetch API's Response: a status, headers (a list of lowercase names and values, with one entry per Set-Cookie), and the body as a binary.
Why an import stopped, as a code the dashboard says in its own words.
Short links and their history from other shorteners: Umami, Dub, Bitly, Short.io, and Rebrandly (the SDK's importers). The dashboard drives an import a step at a time; each step fetches the next few links, writes each with its history, and reports progress, with a cursor to carry on from. Credentials come with every step and are never stored.
Visit history from Umami or a CSV file: pageviews and custom events with where each visit came from, its place, and its device, written as imported visits so the dashboard's history does not start the day Runlight was installed (the SDK's importers/visits.ts and csvvisits.ts).
A JSON object that keeps its keys in the order they were set, as a
JavaScript object does, so answers serialise byte for byte as the
TypeScript library's. Rows from the store and the bodies of the API's
answers are these. It implements Access, so row["name"] reads a key.
A link that cannot be made. code and params let the dashboard say it in its own language.
Short links: create, change, delete, and import, with the rules every route shares (the SDK's links.ts). A link is a JavaScript object: id, site, domain ("" for the app's own), slug, name, url, createdAt, and updatedAt.
A mail problem to show the person setting it up. code and params let the
dashboard say it in its own language; a service's own words travel in
params["detail"].
A reader for MaxMind DB files (the MMDB format that MaxMind's GeoLite2 and DB-IP's free databases use), in plain Elixir so location needs no library. It answers what the TypeScript server's mmdb-lib answers: the record for an address, maps with string keys, or nil when the address is not in the database.
The dashboard, its API, and the tracker as a Plug: the SDK's
rl.routes(), the same answers byte for byte, so the dashboard, the MCP
server, and every port's tools work against an Elixir app as they do
against a Node one.
For an endpoint (before the router): a request on a link domain added in Settings, such as go.example.com, is answered with its redirect or a 404; every other request carries on.
Short links on the app's own domain: forward the instance's link path
(default "/go") here, and /go/{slug} redirects to the link's
destination, with the click recorded. A request on a link domain is
answered as that domain's links first.
Records page requests from known AI agents, which run no JavaScript, so the tracker cannot see them. Put it in a pipeline. It answers a request on a link domain with its redirect or a 404 and never stops any other request.
JavaScript's RangeError, for the few refusals that carry no code of their own, such as an unknown link.
The dashboard, its JSON API, the tracker's script and endpoint, short links' API, OAuth, and the MCP server, under one base path (the SDK's routes.ts). Every answer is the SDK's byte for byte.
Fetches from addresses that other people's input names, such as the icon links on a site's home page or a link domain, and only from the public internet (the SDK's safefetch.ts). Only https is fetched, never a private, loopback, link-local, or metadata address, and redirects are followed by hand under the same rules.
A URL's query as the WHATWG URLSearchParams reads it: an ordered list of
{name, value} pairs, where a name may repeat.
A setting refused, such as a site's domain or the assistant's service, with a code the dashboard says in its own words.
Runlight's tables in a SQL database: SQLite, Postgres, or MySQL and MariaDB. The same tables, columns, and statements as the TypeScript SDK's SqlStore, so one database can be served by any implementation.
A URL parsed as the WHATWG URL standard parses one, as new URL() does in
JavaScript, so every port reads a request's address the same way.