For revenue engineers

Shape your customer data at agent speed

Brindle gives revenue engineers the building blocks for a customer system that fits how the business sells, and reshapes itself as the business changes.

Data model A graph of four CRM objects, account, contact, deal and task, with their records round them; a fifth object grows on the right and a link runs to it from the account, then records appear on it. // a faint link: a soft band from a to b, at least a column wide and a cell tall, so it never falls between samples float lk(vec2 p, vec2 a, vec2 b, float k) { vec2 ab = b - a; float h = clamp(dot(p - a, ab) / dot(ab, ab), 0.0, 1.0); vec2 d = (p - a - ab * h) / vec2(3.6, 4.8); return k * (1.0 - sm(0.8, 1.05, length(d))); } // an object hub: a bright rim, an inner ring, a speckled floor and a dense core that churns float hubf(vec2 p, vec2 c, float R, float fi, float ph) { float d = length(p - c) / R; float inside = 1.0 - sm(0.9, 1.0, d); float churn = noise(p * 0.09 + vec2(5.0 * fi + 1.4 * cos(ph), 3.0 * fi + 1.4 * sin(ph))); float v = 0.62 * exp(-pow(d - 1.0, 2.0) / 0.01); v += inside * 0.3 * exp(-pow(d - 0.62, 2.0) / 0.006); v += inside * 0.3; v += blob(p, c, R * 0.5, 1.0 + 0.5 * (churn - 0.5)); return v; } // records round a hub: up to eight small points on a loose ring, the first cnt of them shown float hosts(vec2 p, vec2 c, float R, float seed, float ph, float cnt) { float v = 0.0; for (int i = 0; i < 8; i++) { float fi = float(i); float h1 = hash(vec2(fi, seed)); float h2 = hash(vec2(seed + 4.0, fi + 9.0)); float a = 6.2832 * (fi + h1) / 8.0 + 0.22 * sin(ph + fi * 1.7); vec2 q = c + vec2(cos(a), sin(a) * 0.82) * R * (1.8 + 0.9 * h2); v += sm(fi - 0.5, fi + 0.2, cnt) * blob(p, q, 6.0, 1.0); } return v; } float bar(vec2 p, float x0, float top, float on) { return on * step(abs(p.x - x0 - 3.5), 4.5) * step(top, p.y) * step(p.y, 421.0) * (0.62 + 0.5 * step(p.y, top + 6.0)); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0) return 0.0; float ph = TAU * t / T; float v = 0.0; // the preview: grows 0.9 to 1.7 s, discarded 5.0 to 5.8 s float env = ease(sm(0.9, 1.7, t)) * (1.0 - ease(sm(5.0, 5.8, t))); // the four objects and their links float ln = 0.0; ln = max(ln, lk(p, vec2(285.0, 215.0), vec2(100.0, 125.0), 0.4)); ln = max(ln, lk(p, vec2(285.0, 215.0), vec2(190.0, 352.0), 0.4)); ln = max(ln, lk(p, vec2(285.0, 215.0), vec2(470.0, 105.0), 0.4)); ln = max(ln, lk(p, vec2(100.0, 125.0), vec2(190.0, 352.0), 0.4)); ln = max(ln, lk(p, vec2(470.0, 105.0), vec2(190.0, 352.0), 0.4)); v += ln; v += hubf(p, vec2(285.0, 215.0), 44.0, 0.0, ph); v += hosts(p, vec2(285.0, 215.0), 44.0, 1.0, ph, 8.0); v += hubf(p, vec2(100.0, 125.0), 30.0, 1.0, ph); v += hosts(p, vec2(100.0, 125.0), 30.0, 2.0, ph, 8.0); v += hubf(p, vec2(190.0, 352.0), 36.0, 2.0, ph); v += hosts(p, vec2(190.0, 352.0), 36.0, 3.0, ph, 8.0); v += hubf(p, vec2(470.0, 105.0), 28.0, 3.0, ph); v += hosts(p, vec2(470.0, 105.0), 28.0, 4.0, ph, 8.0); // RENEWAL: declared, then grown from a point; its ring goes out as it settles vec2 N = vec2(585.0, 228.0); v += env * hubf(p, N, 38.0 * (0.35 + 0.65 * ease(sm(0.9, 1.7, t))), 7.0, ph); float g = t - 1.3; if (g > 0.0 && g < 0.7) v += 0.55 * (1.0 - g / 0.7) * exp(-pow(length((p - N) * vec2(1.0, 1.2)) - 30.0 - 60.0 * g, 2.0) / 28.0); // the link and its packet: the packet leaves ACCOUNT at 1.9 s and arrives at 2.9 s; the link lights on arrival { float u = clamp((t - 1.9) / 1.0, 0.0, 1.0); vec2 A = vec2(285.0, 215.0); vec2 c = mix(A, N, ease(u)); float vis = (1.0 - ease(sm(5.0, 5.8, t))); if (u > 0.0) { v += vis * lk(p, A, c, 0.62); if (u < 1.0) v += blob(p, c, 8.0, 1.35); v += vis * sm(0.97, 1.0, u) * blob(p, N, 30.4, 0.4); } float a2 = t - 2.9; if (a2 > 0.0 && a2 < 0.4) v += 0.6 * (1.0 - a2 / 0.4) * exp(-pow(length(p - N) - 36.0 - 70.0 * a2, 2.0) / 24.0); } // records appear on RENEWAL one by one from 3.0 s v += env * hosts(p, N, 38.0, 8.0, ph, 8.0 * ease(sm(3.0, 4.4, t))); // the strip: empty slots, then a bar per step once it is done if (p.x > 436.0 && p.x < 574.0 && p.y > 348.0 && p.y < 421.0) { float slot = mod(p.x - 441.0, 44.0); v += slot < 8.0 ? 0.13 : 0.0; } v += bar(p, 441.0, 386.0, sm(1.65, 1.75, t) * (1.0 - sm(5.0, 5.8, t))); v += bar(p, 485.0, 368.0, sm(2.85, 2.9499999999999997, t) * (1.0 - sm(5.0, 5.8, t))); v += bar(p, 529.0, 350.0, sm(4.3500000000000005, 4.45, t) * (1.0 - sm(5.0, 5.8, t))); return v; }
  • Open-source coreRead every line and run it on your own servers.
  • Live in an hourA first schema and real records with the local setup.
  • Agent readyA schema and command reference your agents can read.

The problem

A custom system gives your team an edge, but building one comes with tradeoffs

Tradeoffs On the left a wide slab of five sealed layers that a change has to step down one at a time; on the right six small in-house services joined by thin links, where one link fails and stalls the change until it is mended. // a faint link: a soft band from a to b, at least a column wide and a cell tall, so it never falls between samples float lk(vec2 p, vec2 a, vec2 b, float k) { vec2 ab = b - a; float h = clamp(dot(p - a, ab) / dot(ab, ab), 0.0, 1.0); vec2 d = (p - a - ab * h) / vec2(3.6, 4.8); return k * (1.0 - sm(0.8, 1.05, length(d))); } // an object hub: a bright rim, an inner ring, a speckled floor and a dense core that churns float hubf(vec2 p, vec2 c, float R, float fi, float ph) { float d = length(p - c) / R; float inside = 1.0 - sm(0.9, 1.0, d); float churn = noise(p * 0.09 + vec2(5.0 * fi + 1.4 * cos(ph), 3.0 * fi + 1.4 * sin(ph))); float v = 0.62 * exp(-pow(d - 1.0, 2.0) / 0.01); v += inside * 0.3 * exp(-pow(d - 0.62, 2.0) / 0.006); v += inside * 0.3; v += blob(p, c, R * 0.5, 1.0 + 0.5 * (churn - 0.5)); return v; } // records round a hub: up to eight small points on a loose ring, the first cnt of them shown float hosts(vec2 p, vec2 c, float R, float seed, float ph, float cnt) { float v = 0.0; for (int i = 0; i < 8; i++) { float fi = float(i); float h1 = hash(vec2(fi, seed)); float h2 = hash(vec2(seed + 4.0, fi + 9.0)); float a = 6.2832 * (fi + h1) / 8.0 + 0.22 * sin(ph + fi * 1.7); vec2 q = c + vec2(cos(a), sin(a) * 0.82) * R * (1.8 + 0.9 * h2); v += sm(fi - 0.5, fi + 0.2, cnt) * blob(p, q, 6.0, 1.0); } return v; } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0) return 0.0; float ph = TAU * t / T; float v = 0.0; float vis = 1.0 - ease(sm(6.2, 6.9, t)); // the monolith: five ribbed layers in a slab with a bright rim float s = clamp((t - 0.4) / 5.0, 0.0, 1.0) * 4.0; float kk = min(floor(s), 3.0); float py = 128.0 + 58.0 * (kk + ease(s - kk)); for (int i = 0; i < 5; i++) { float fi = float(i); float cy = 128.0 + 58.0 * fi; float inb = step(abs(p.y - cy), 22.0) * step(40.0, p.x) * step(p.x, 290.0); float rib = 0.5 + 0.5 * step(0.5, fract((p.x - 40.0) / 28.0)); float lit = vis * sm(fi - 0.08, fi, s) * step(0.0, t - 0.4); float rim = step(abs(abs(p.y - cy) - 22.0), 3.5) * step(40.0, p.x) * step(p.x, 290.0); v += inb * (0.34 * rib + 0.5 * lit * (0.7 + 0.3 * noise(p * 0.07 + fi))) + 0.3 * rim * (0.4 + lit); } v += step(40.0, p.x) * step(p.x, 290.0) * step(abs(p.y - 244.0), 150.0) * 0.1 * (0.6 + 0.4 * sin(ph + p.x * 0.05)); // the change request: a dense packet stepping down through the layers float pk = step(0.4, t) * step(t, 5.5) * (1.0 - ease(sm(5.3, 5.5, t))); v += pk * blob(p, vec2(165.0, py), 10.0, 1.3); // the in-house services and their links float fail = 1.0 - sm(1.6, 1.7, t) * (1.0 - sm(4.2, 4.4, t)); fail *= t > 1.6 && t < 1.9 ? 0.55 + 0.45 * sin(t * 140.0) : 1.0; float ln = 0.0; ln = max(ln, lk(p, vec2(410.0, 120.0), vec2(505.0, 200.0), 0.27)); ln = max(ln, lk(p, vec2(505.0, 200.0), vec2(610.0, 135.0), 0.27)); ln = max(ln, lk(p, vec2(505.0, 200.0), vec2(495.0, 350.0), 0.27)); ln = max(ln, lk(p, vec2(505.0, 200.0), vec2(615.0, 305.0), 0.27) * fail); ln = max(ln, lk(p, vec2(615.0, 305.0), vec2(495.0, 350.0), 0.27)); ln = max(ln, lk(p, vec2(405.0, 260.0), vec2(505.0, 200.0), 0.27)); ln = max(ln, lk(p, vec2(405.0, 260.0), vec2(410.0, 120.0), 0.27)); ln = max(ln, lk(p, vec2(405.0, 260.0), vec2(495.0, 350.0), 0.27)); ln = max(ln, lk(p, vec2(610.0, 135.0), vec2(615.0, 305.0), 0.27)); v += ln; vec2 mid = (vec2(505.0, 200.0) + vec2(615.0, 305.0)) * 0.5; v += blob(p, mid, 11.0, 1.2 * exp(-pow(t - 1.72, 2.0) / 0.006)); v += blob(p, mid, 11.0, 1.1 * exp(-pow(t - 4.3, 2.0) / 0.012)); v += hubf(p, vec2(410.0, 120.0), 21.0, 3.0, ph); v += hubf(p, vec2(505.0, 200.0), 24.0, 4.0, ph); v += hubf(p, vec2(610.0, 135.0), 20.0, 5.0, ph); v += hubf(p, vec2(615.0, 305.0), 24.0, 6.0, ph); v += hubf(p, vec2(495.0, 350.0), 21.0, 7.0, ph); v += hubf(p, vec2(405.0, 260.0), 18.0, 8.0, ph); // the in-house change: R0 to R1, stalls at the failed link, then R1 to R3 to R4 once it is mended { float a = clamp((t - 0.4) / 1.0, 0.0, 1.0); vec2 c0 = mix(vec2(410.0, 120.0), vec2(505.0, 200.0), ease(a)); float on0 = step(0.4, t) * (1.0 - ease(sm(1.5, 1.8, t))) * vis; v += on0 * (blob(p, c0, 8.0, 1.3) + lk(p, vec2(410.0, 120.0), c0, 0.55)); float b = clamp((t - 4.4) / 0.8, 0.0, 1.0); vec2 c1 = mix(vec2(505.0, 200.0), vec2(615.0, 305.0), ease(b)); float on1 = step(4.4, t) * vis * (1.0 - step(5.2, t)); v += on1 * (blob(p, c1, 8.0, 1.3) + lk(p, vec2(505.0, 200.0), c1, 0.6)); float c = clamp((t - 5.2) / 0.6, 0.0, 1.0); vec2 c2 = mix(vec2(615.0, 305.0), vec2(495.0, 350.0), ease(c)); float on2 = step(5.2, t) * vis; v += on2 * (blob(p, c2, 8.0, 1.3) + lk(p, vec2(615.0, 305.0), c2, 0.6) + lk(p, vec2(505.0, 200.0), vec2(615.0, 305.0), 0.6)); v += vis * step(1.4, t) * (1.0 - step(1.8, t)) * 0.0; } return v; }

The sealed monolith

Closed rule languages, release trains measured in quarters, and logic nobody can open.

The homegrown patchwork

It is brittle. The first version ships fast, but every change after that is a slow tax on the team that wrote it.

Stop settling for trade-offs

Assemble, reshape and extend a sturdy customer system that bends without breaking

Compose your pipeline and internal tools with a single set of building blocks.

Building blocks Nine ribbed blocks are laid into three columns, data model, layout and logic, counted on a strip below; then a record runs from the first block to the second to the workflow block and the run is confirmed. // a faint link: a soft band from a to b, at least a column wide and a cell tall, so it never falls between samples float lk(vec2 p, vec2 a, vec2 b, float k) { vec2 ab = b - a; float h = clamp(dot(p - a, ab) / dot(ab, ab), 0.0, 1.0); vec2 d = (p - a - ab * h) / vec2(3.6, 4.8); return k * (1.0 - sm(0.8, 1.05, length(d))); } // an object hub: a bright rim, an inner ring, a speckled floor and a dense core that churns float hubf(vec2 p, vec2 c, float R, float fi, float ph) { float d = length(p - c) / R; float inside = 1.0 - sm(0.9, 1.0, d); float churn = noise(p * 0.09 + vec2(5.0 * fi + 1.4 * cos(ph), 3.0 * fi + 1.4 * sin(ph))); float v = 0.62 * exp(-pow(d - 1.0, 2.0) / 0.01); v += inside * 0.3 * exp(-pow(d - 0.62, 2.0) / 0.006); v += inside * 0.3; v += blob(p, c, R * 0.5, 1.0 + 0.5 * (churn - 0.5)); return v; } // records round a hub: up to eight small points on a loose ring, the first cnt of them shown float hosts(vec2 p, vec2 c, float R, float seed, float ph, float cnt) { float v = 0.0; for (int i = 0; i < 8; i++) { float fi = float(i); float h1 = hash(vec2(fi, seed)); float h2 = hash(vec2(seed + 4.0, fi + 9.0)); float a = 6.2832 * (fi + h1) / 8.0 + 0.22 * sin(ph + fi * 1.7); vec2 q = c + vec2(cos(a), sin(a) * 0.82) * R * (1.8 + 0.9 * h2); v += sm(fi - 0.5, fi + 0.2, cnt) * blob(p, q, 6.0, 1.0); } return v; } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0) return 0.0; float ph = TAU * t / T; float s = mod(t + 5.2, T); float v = 0.0; float clear = 0.0; for (int i = 0; i < 9; i++) { float fi = float(i); float c = floor(fi / 3.0); float r = fi - 3.0 * c; float x0 = 50.0 + 235.0 * c; float y0 = 118.0 + 74.0 * r; float t0 = 0.3 + 0.26 * fi; float pr = ease(sm(t0, t0 + 0.5, s)) * (1.0 - ease(sm(5.6 + 0.05 * fi, 6.1 + 0.05 * fi, s))); // the empty slot: a faint outline that is always there float rimS = step(abs(abs(p.x - x0 - 75.0) - 75.0), 3.5) * step(abs(p.y - y0 - 26.0), 26.0) + step(abs(abs(p.y - y0 - 26.0) - 26.0), 3.5) * step(abs(p.x - x0 - 75.0), 75.0); v += rimS * 0.17; // the slab, risen from below by 36 px as it lands vec2 q = p - vec2(x0, y0 + 36.0 * (1.0 - pr)); float inb = step(0.0, q.x) * step(q.x, 150.0) * step(0.0, q.y) * step(q.y, 52.0) * pr; float rib = 0.55 + 0.35 * step(0.5, fract((q.x - 30.0) / 21.0)) * step(30.0, q.x); float handle = step(q.x, 26.0) * 0.55; float rim = step(abs(abs(q.y - 26.0) - 26.0), 3.5) * 0.35 + step(abs(q.x - 148.0), 3.5) * 0.2; v += inb * (0.42 * rib + handle + rim + 0.06 * sin(ph * 2.0 + q.x * 0.1 + fi)); // lit once the record reaches it float arr = i == 0 ? 3.1 : (i == 3 ? 4.05 : (i == 8 ? 5.0 : 99.0)); float lit = sm(arr, arr + 0.15, s) * (1.0 - ease(sm(5.6, 6.1, s))); v += inb * lit * 0.45; // the build strip: one square a slab, filled once it has landed float tk = step(abs(p.x - 52.0 - 24.0 * fi), 5.0) * step(372.0, p.y) * step(p.y, 391.0); v += tk * (0.14 + 0.85 * pr * step(0.97, pr)); } // a scan that crosses the slots all the time, so the empty frame still moves float sw = fract(t / T); v += 0.3 * sin(3.1416 * sw) * exp(-pow(p.x - (18.0 + 684.0 * sw), 2.0) / 500.0) * step(abs(p.y - 232.0), 150.0) * step(0.5, fract((p.y - 118.0) / 74.0) < 0.7 ? 1.0 : 0.0); // the record: leaves the first block at 3.2 s, reaches the second at 4.05 s, the workflow block at 5.0 s float ru = clamp((s - 3.2) / 0.85, 0.0, 1.0); float rv = clamp((s - 4.15) / 0.85, 0.0, 1.0); vec2 pa = vec2(125.0, 144.0); vec2 pb = vec2(360.0, 144.0); vec2 pc = vec2(595.0, 292.0); vec2 pos = rv > 0.0 ? mix(pb, pc, ease(rv)) : mix(pa, pb, ease(ru)); float fade = 1.0 - ease(sm(5.0, 5.3, s)); float on = step(3.2, s) * fade; v += on * (blob(p, pos, 9.0, 1.4) + lk(p, pa, mix(pa, pb, ease(ru)), 0.6) + (rv > 0.0 ? lk(p, pb, pos, 0.6) : 0.0)); float g = s - 5.0; if (g > 0.0 && g < 0.45) v += 0.6 * (1.0 - g / 0.45) * exp(-pow(length((p - pc) * vec2(1.0, 1.6)) - 40.0 - 70.0 * g, 2.0) / 30.0); return v; }
  1. Begin with sturdy building blocks

    Compose your pipeline and internal tools from one set of parts. Data model, layout, and logic.

  2. Keep going without friction

    Customize freely with the coding agents you already use. Reshape the system to match how your team sells and grows.

  3. Stay in charge with open source

    No one else's ecosystem to be locked into. Brindle feels like ordinary software: local setup, real data, live tests, and no proprietary tooling.

In practice

What teams changed once the frame was already built

Production grade quality

A freight company treated Brindle as the finished frame for objects, roles, sign-in and automation, instead of rebuilding each piece from scratch.

Head of Platform, freight company

Agents for quick iteration

An advisory firm pointed coding agents at their schema and folded a quarter of renewal-tracking work into a week, with one engineer reviewing.

Managing Partner, advisory firm

Control without drag

A clinic group moved to a self-hosted Brindle with no vendor lock, no forced upgrade path, and system costs down by more than 80%.

Systems Lead, clinic group

Skip the rough edges that come with custom

Give your sales team a system they will enjoy

Table view A table of six rows with name, stage and value bars; a filter fades the rows that do not match and the three that remain slide up together. // a bar of a row: height 14, from x0 for len, with a dense cap at its end float rowb(vec2 p, float y, float x0, float len, float k) { float inb = step(abs(p.y - y), 7.0) * step(x0, p.x) * step(p.x, x0 + len); return inb * (k + 0.35 * step(x0 + len - 7.0, p.x)); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0) return 0.0; float ph = TAU * t / T; float v = 0.0; float e = ease(sm(2.0, 2.7, t)) * (1.0 - ease(sm(4.6, 5.3, t))); float gone = ease(sm(1.4, 1.9, t)) * (1.0 - ease(sm(5.3, 5.8, t))); // header: three thin bars over the columns v += rowb(p, 84.0, 60.0, 90.0, 0.2) + rowb(p, 84.0, 290.0, 60.0, 0.2) + rowb(p, 84.0, 450.0, 80.0, 0.2); v += step(abs(p.y - 100.0), 1.6) * step(50.0, p.x) * step(p.x, 660.0) * 0.3; for (int i = 0; i < 6; i++) { float fi = float(i); float keep = mod(fi, 2.0) < 0.5 ? 1.0 : 0.0; float slot = keep > 0.5 ? fi * 0.5 : fi; float y = 130.0 + 54.0 * mix(fi, slot, e * keep); float vis = keep > 0.5 ? 1.0 : 1.0 - gone; float h = hash(vec2(fi, 3.0)); float pulse = 0.06 * sin(ph + fi * 1.3); v += vis * (rowb(p, y, 60.0, 100.0 + 70.0 * h, 0.5 + pulse) + rowb(p, y, 290.0, 40.0 + 30.0 * h, 0.9) + rowb(p, y, 450.0, 60.0 + 150.0 * (1.0 - h), 0.38 + pulse)); v += vis * step(abs(p.y - y - 26.0), 1.2) * step(50.0, p.x) * step(p.x, 660.0) * 0.14; } return v; }

Familiar, tidy tables

Brindle keeps it plain. Rows, columns and filters behave the way you expect, and narrow to what matters in a click.

Live sync Two panes of rows side by side; one row is edited on the left, a packet crosses to the right pane, and the matching row there grows once it arrives. // a bar of a row: height 14, from x0 for len, with a dense cap at its end float rowb(vec2 p, float y, float x0, float len, float k) { float inb = step(abs(p.y - y), 7.0) * step(x0, p.x) * step(p.x, x0 + len); return inb * (k + 0.35 * step(x0 + len - 7.0, p.x)); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0) return 0.0; float ph = TAU * t / T; float v = 0.0; float back = 1.0 - ease(sm(4.4, 5.2, t)); float gl = ease(sm(0.7, 1.4, t)) * back; float gr = ease(sm(2.3, 2.9, t)) * back; for (int i = 0; i < 4; i++) { float fi = float(i); float y = 120.0 + 72.0 * fi; float h = hash(vec2(fi, 8.0)); float ll = 90.0 + 80.0 * h + (fi == 1.0 ? 130.0 * gl : 0.0); float lr = 90.0 + 80.0 * h + (fi == 1.0 ? 130.0 * gr : 0.0); v += rowb(p, y, 36.0, ll, 0.5 + 0.05 * sin(ph + fi)) + rowb(p, y + 24.0, 36.0, 80.0 + 60.0 * h, 0.22); v += rowb(p, y, 372.0, lr, 0.5 + 0.05 * sin(ph + fi + 1.0)) + rowb(p, y + 24.0, 372.0, 80.0 + 60.0 * h, 0.22); } // the two panes' edges v += step(abs(p.x - 352.0), 1.6) * step(abs(p.y - 232.0), 150.0) * 0.18; // the cursor riding the end of the edited bar while it grows float cx = 36.0 + 90.0 + 80.0 * hash(vec2(1.0, 8.0)) + 130.0 * gl; v += step(0.7, t) * step(t, 1.8) * blob(p, vec2(cx + 8.0, 192.0), 8.0, 1.1 * (1.0 - ease(sm(1.4, 1.8, t)))); // the packet: leaves the edited row once the edit is made, lands on the right row float u = clamp((t - 1.6) / 0.7, 0.0, 1.0); vec2 A = vec2(cx + 10.0, 192.0); vec2 B = vec2(366.0, 192.0); float on = step(1.6, t) * (1.0 - ease(sm(2.3, 2.6, t))); v += on * (blob(p, mix(A, B, ease(u)), 8.5, 1.35) + step(abs(p.y - 192.0), 3.0) * step(A.x, p.x) * step(p.x, mix(A.x, B.x, ease(u))) * 0.45); float g = t - 2.3; if (g > 0.0 && g < 0.45) v += 0.6 * (1.0 - g / 0.45) * exp(-pow(length((p - vec2(480.0, 192.0)) * vec2(0.5, 1.5)) - 20.0 - 50.0 * g, 2.0) / 30.0); return v; }

Live data, shared

An edit made by one person shows up for everyone at once, with no refresh and no merge to resolve.

Command bar A query field with typed text over a list of rows; the list narrows to two, the top one is picked, and a packet carries it to an action pane that answers. // a bar of a row: height 14, from x0 for len, with a dense cap at its end float rowb(vec2 p, float y, float x0, float len, float k) { float inb = step(abs(p.y - y), 7.0) * step(x0, p.x) * step(p.x, x0 + len); return inb * (k + 0.35 * step(x0 + len - 7.0, p.x)); } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0) return 0.0; float ph = TAU * t / T; float v = 0.0; float clr = 1.0 - ease(sm(4.8, 5.6, t)); // the query field: an outline, and the typed text growing in three steps v += step(abs(abs(p.y - 86.0) - 24.0), 1.6) * step(28.0, p.x) * step(p.x, 460.0) * 0.28; float typed = 24.0 * (step(0.5, t) + step(0.9, t) + step(1.3, t)) * clr; v += rowb(p, 86.0, 44.0, 18.0 + typed, 0.55); float blink = 0.5 + 0.5 * cos(TAU * 2.0 * t); v += step(abs(p.y - 86.0), 11.0) * step(abs(p.x - (68.0 + typed)), 2.0) * (0.3 + 0.5 * blink); // the list: five rows; rows 1, 2 and 4 do not match and fade; the others close up float e = ease(sm(2.0, 2.5, t)) * (1.0 - ease(sm(4.8, 5.4, t))); float gone = ease(sm(1.5, 2.0, t)) * (1.0 - ease(sm(5.4, 5.8, t))); float pick = sm(2.6, 2.75, t) * (1.0 - ease(sm(4.8, 5.4, t))); for (int i = 0; i < 5; i++) { float fi = float(i); float keep = (i == 0 || i == 3) ? 1.0 : 0.0; float slot = i == 3 ? 1.0 : fi; float y = 150.0 + 52.0 * mix(fi, slot, e * keep); float vis = keep > 0.5 ? 1.0 : 1.0 - gone; float h = hash(vec2(fi, 5.0)); float lit = (i == 0) ? pick : 0.0; v += vis * (rowb(p, y, 44.0, 210.0 + 120.0 * h, 0.4 + 0.5 * lit + 0.05 * sin(ph + fi)) + rowb(p, y, 28.0, 10.0, 0.5)); v += vis * rowb(p, y, 392.0, 20.0 + 28.0 * h, 0.22); } // the action pane: a block that answers once the packet lands float ans = ease(sm(3.65, 4.1, t)) * clr; v += step(abs(abs(p.x - 590.0) - 106.0), 1.6) * step(abs(p.y - 232.0), 120.0) * 0.22; v += step(abs(p.x - 590.0), 106.0) * step(abs(p.y - 232.0), 120.0) * (0.1 + 0.35 * ans) * (0.7 + 0.3 * noise(p * 0.06 + ph)); v += blob(p, vec2(590.0, 232.0), 34.0, 0.9 * ans); // the packet: from the lit row to the pane float u = clamp((t - 3.0) / 0.7, 0.0, 1.0); float on = step(3.0, t) * (1.0 - ease(sm(3.7, 4.0, t))); vec2 A = vec2(400.0, 150.0); vec2 B = vec2(500.0, 232.0); v += on * blob(p, mix(A, B, ease(u)), 8.5, 1.35); float g = t - 3.7; if (g > 0.0 && g < 0.5) v += 0.6 * (1.0 - g / 0.5) * exp(-pow(length(p - vec2(590.0, 232.0)) - 50.0 - 55.0 * g, 2.0) / 28.0); return v; }

Straight to the action

Type a few letters, pick a match, and the action runs. Shortcuts and layouts keep daily work short.

In production

Revenue teams drive company-wide change with Brindle

Freight company

Ship a product on Brindle

The team built a driver-onboarding portal at scale, with Brindle as the operational backbone.

“We stopped asking for a feature and started shipping it ourselves. The first week we changed the data model three times and nothing broke.”

Tobias VerhagenHead of Platform

Read the case

Clinic group

Own your system end to end

The group replaced a retired vendor system with a self-hosted Brindle and cut running costs by more than 80%.

“The flexibility is what made the difference. Our needs shift week to week. When I spot a new one, two clicks later it is handled. That is a real advantage when you are moving fast.”

Maren OduyaCo-founder

Read the case

Advisory firm

Grow on a flexible foundation

The firm runs a modular Brindle setup across renewals, referrals and reporting for four practice areas.

“Self-hosting meant our records stayed ours. The agents do the tedious mapping, and one of us reviews it over coffee.”

Ines CallowayManaging Partner

Read the case

Any questions?

Things teams ask before they switch

Is Brindle really open-source?

Yes. The core is published under a permissive licence, you can read every line, and you can run it on your own servers.

How long does it take to get started?

Most teams have a first schema and real records in under an hour with the local setup, and a shared workspace by the end of the day.

Can I migrate from my current CRM?

Yes. Import maps your existing objects and fields, and an agent can draft the mapping for you to review before anything is written.

Do I need a developer to customize Brindle?

Not for everyday changes: objects, fields and views are point and click. A developer helps when you want custom logic.

Can developers extend Brindle with code?

Yes. Blocks are plain TypeScript, tested and deployed like the rest of your software.

Does Brindle work with the coding agents I use?

It ships a schema and command reference that agents read directly, so they can propose changes you review as ordinary diffs.

What does Brindle cost?

Self-hosting is free. Hosted workspaces are priced per seat, with a flat rate for read-only users.

Stop fighting custom. Start building with Brindle.