Elixir · Ash · Phoenix · Postgres · MIT

The hard 80% of a B2B SaaS. Solved once. Proven on every commit.

Samen is a foundry for launching many B2B products from one substrate. Customer data is masked by default, support sees it only through a second person's approval, erasure destroys keys instead of rows, and the AI agent can only propose. One command emits a running product that passes its own gate. Every guarantee ships with a sabotage that proves its test can fail.

604
committed sabotages, each proving a named test can fail
ls scripts/sabotages/*.patch
25
verifiers in the gate, from no_plaintext_pii to no_pan_columns
mix samen.verify.*
18
scope blueprints a product mounts as-is: CRM, billing, support, work, automation…
samen_core/lib/samen/scopes
0
hand-edits for a generated app to pass its own 19-step gate
gen_app_flagship_probe.exs
00The whole story

Every B2B product rebuilds the same 80%. And the same leak with it.

Auth, billing, a CRM, a support desk, files, search, exports, an admin tool for your own staff. Every one of those surfaces is also a door onto customer data, each with its own idea of who may open it. Samen builds the 80% once, at the substrate, and makes the safe answer the default value of a field, not a feature someone remembers to add. Then it tries to break every promise it makes.

Act I · Start

One command, a running product

Not a scaffold. A thing you can boot, with its own gate.

Act II · Trust, by construction

Customer data that defaults to safe

Seven features that only work because the planes share one core.

Act III · The rest of the 80%

Everything else a B2B SaaS needs

Surfaces, vendors and a cockpit for running many products.

Act IV · Proof

A test that cannot fail is a bug

Sabotages, mutants, and CI an agent can afford to run.

Act I · Start

The proof that a substrate is real is generative.

01Generate

One command. A product you can boot.

mix samen.gen.app doesn't hand you a folder of TODOs. It emits a product with a web UI, a JSON:API, seeds, observability, tests and its own verifier gate, and that app passes the gate on its first run with nothing edited in between.

19steps in the generated app's own gate, all green on the first run, zero hand-edits. re-run every PR
  • Billing, notifications, flags, an operator workspace and a deny-by-default API, mounted, not copied
  • One authored resource with a real PII field, so the vault and reveal path are live from the first migration
  • Four generators: app, scope, resource and agent, plus a fail-honest --deploy
# emit a new vertical from the substrate$ mix samen.gen.app --module Harbor --prefix hb --abbrev hrb * creating harbor/ web · api · seeds · telemetry · ci.sh * mounting Billing · Notifications · Flags · Operator desk * reserving abbrev hrb (permanent, like a ticker) # run the generated app's own gate. nothing edited.$ cd ../harbor && MIX_ENV=test bash ci.sh ✓ prefixes · catalog_parity · vault_declared_parity ✓ no_plaintext_pii · no_pii_columns · pii_reads ✓ api_contract · ai_prompt_masking · tnt_boundary ✓ migrations · same_org_fk · oban_queues · …19/19 steps ALL PASSED zero hand-edits $ mix harbor.seed && mix phx.server→ http://localhost:4050 ▍

The root gate does exactly this on every PR: generate a fresh app, run its whole gate, seed it, boot it, probe its routes, then sabotage it.

Act II · Trust, by construction

Seven features that only work because both planes share one core.

Most products guard customer data in the places someone thought of. Samen moves the decision into the field itself, then builds every feature on top of that one decision.

02The vault

•••• isn't a redaction. It's the value.

Declare pii_attribute :email and the value goes to a per-subject-keyed vault. The row keeps a vt_… token; plaintext is nowhere at rest. Every reader gets %Masked{} back: the UI, the API, the CSV writer, the log line. Forgetting to mask is not a mistake available to you.

1decrypt path in the whole system. One function to gate, one to log, one for a reviewer to read. built
  • Validated, then tokenized: Money, Email, Phone, Address… each type's cast runs before encryption
  • CSV masks by omission: the column isn't in the file, so there's nothing to un-redact
  • A verifier in a separate OS process attests no tier holds plaintext
03Reveal

Support can see PII. Only with a second person's yes.

A dispatcher needs the address that bounced. Pretending support never sees data just moves the work into a spreadsheet nobody audits. Samen keeps the exception inside the system and makes it expensive, visible and self-expiring.

2people, always. The requester cannot approve: a database CHECK makes that row impossible, not just a form rule. built
  • Time-boxed: the auto-revoke job commits in the same transaction as the approval
  • Never renewed in place: an extension is a new request and a new approval
  • Tenant-readable: a hash-chained log the operator cannot edit, the tenant can read without asking
04Two planes, one core

Your admin tool is the biggest leak in the company. Unless it's the same app.

The operator plane isn't a second application with its own queries. It's the same objects rendered for a different actor. Support sees the real UI, the real layout, the real bug. The PII inside it renders ••••.

3actors, one substrate. What you can see is a property of you, not of the page. built
  • Tenant: reads its own org in the clear; scoping is enforced at the resource, not per query
  • Operator: masked impersonation, grant-capable, every read in the tenant's log
  • Aggregate: token-blind, over resources with zero PII columns
In the productdriftwood, the freight vertical · one Blue Ridge Logistics roster, three actors

Switch the actor. The rows don't move; what resolves inside them does.

Rosa dispatches for Blue Ridge, so her actor is a tenant member of that org: every vaulted field on her own drivers resolves in the clear. No grant, no ceremony; this is the ordinary case.

Support opened a masked impersonation session with a reason. Same drivers, same order, same FMCSA badges, read through the same Driftwood.Reads.driver_roster; name and CDL resolve to %Masked{} because the plane is :operator and no grant is held.

The portfolio view runs as the token-blind aggregate actor over projections that have no pii_ column at all. Nothing is masked because nothing is maskable, and a lane with fewer than five tenants is suppressed to ⊘.

Blue Ridge Logistics/Operations/DriversRMRosa Medina
dispatcher
tenant plane
DriversFilter+ New driver
Driver roster5· your org — driver names in the clear
DriverCDL #StateCDL expiryMed cardStatusFMCSADispatch
MVMarcus ValeD4471-8820IL2027-03-142026-11-02on_loadOKDispatch
DWDana WhitfieldD5120-3364MA2028-06-302027-01-18availableOKDispatch
TBTheo BranniganD3388-1907OH2027-09-022026-09-30availableBLOCKED: medical card expiredDispatch (blocked)
PRPriya RaghunathanD6642-5518IL2029-02-112027-05-06out_of_serviceBLOCKED: out of serviceDispatch (blocked)
COCurtis OkaforD2907-4471OH2028-12-012027-08-22on_loadOKDispatch
Freight.Driver · resolvedfull_namecdl_numbercdl_statecdl_expirymedical_card_expirystatus
Operator plane/Impersonation/Blue Ridge Logistics/DriversCKC. Kluis
operator · support
operator plane
Driver rosterFilter
Masked impersonation. Impersonating brokerage Blue Ridge Logistics. PII is masked (••••). Unmasking a subject needs a second-party reveal grant, is time-boxed, and is written to the tenant-readable audit log.Reason: ticket #7781: dispatch dispute · Session expires 14:52grants held: 0
Driver roster5· real tenant data, personal fields render ••••
DriverCDL #CDL stateCDL expiryStatusFMCSAReveal
·••••••••IL2027-03-14on_loadOKReveal driver record Request reveal
·••••••••MA2028-06-30availableOKReveal driver record Request reveal
·••••••••OH2027-09-02availableBLOCKED: medical card expiredReveal driver record Request reveal
·••••••••IL2029-02-11out_of_serviceBLOCKED: out of serviceReveal driver record Request reveal
·••••••••OH2028-12-01on_loadOKReveal driver record Request reveal
Freight.Driver · resolvedfull_name → %Masked{}cdl_number → %Masked{}cdl_statecdl_expirystatus
Operator plane/Aggregate/PortfolioAGoperator_aggregate
token-blind actor, no org
aggregate plane
Portfolio
Token-blind aggregate plane. This actor has no pii_ column in its domain by construction — it reads a vault-excluded projection over tenant orgs. Cohorts below the k-anonymity floor are suppressed (⊘).no reveal path · k-anon suppressed
Portfolio MRR$104,800across tiers
Active tenants42org-level cohorts
Loads / total1,284across all lanes
Total gross$2.41Macross all lanes
Load volume by lane5· org-level aggregates only — no personal data
LaneTenantsLoadsGross
TX->CA14412$771,400
IL->GA11366$648,900
FL->NY9297$602,300
WA->AZ6209$389,700
MT->ND2⊘⊘
1 cohort below the k-anonymity floor — suppressed to prevent re-identification.
Aggregate.LoadVolumeByLane · resolvedlanetenant_countload_countgross_centspii_ columns: none

Pinch to zoom: the desktop screen, scaled to fit.

Redrawn from driftwood's own screens: DriftwoodWeb.BrokerLive, DriftwoodWeb.OperatorImpersonationLive and the framework's Samen.Web.Operator.AggregateLive, with fictional tenant data. Tenant and operator render through the same read layer and the same Samen.Api.PiiResolution resolver; only the actor's plane differs. The FMCSA badge stays readable on both, so support can work the problem without the person.

05Erasure

"Delete my data" is a DELETE. That isn't erasure.

The row goes; the value survives in the audit log, last night's rollup, a queued job's arguments, the CDC stream and the backup. Samen erases by destroying the subject's key: the ciphertext everywhere becomes unreadable at once, including in tiers you forgot you had.

6tiers a separate OS process walks to attest a shredded subject is unrecoverable, including backups, PITR and replay. every CI run
  • A positive over a finite set, not a negative over every copy that might exist
  • The audit chain survives: you can prove a reveal happened, not recover what it showed
  • A new tier that stores plaintext turns CI red instead of slipping past a migration
$ mix samen.verify.no_plaintext_pii --subject drv_8821 spawning verifier in a separate OS process committed state only · no app caches · no warm keys domain rows unreadable pii vault unreadable audit events unreadable (chain intact) rollups unreadable oban args unreadable cdc mirror unreadable backup · PITR unreadable session replay [erased] kms store key material absent ATTESTED drv_8821 is unrecoverable across all tiers

Nothing was walked, scrubbed or rewritten. One destruction, and every tier is unreadable at the same instant.

06Session replay

Watch what happened. Never what they typed.

Session replay is how support finally sees the bug. It's also usually a recording of every value a user typed. Samen's replay records references and shapes: which record, which field, how long the text was. The value never enters the recording, so no viewer can get it back.

0plaintext bytes stored. The same recording plays four ways, by who's watching, and erased subjects render [erased]. built
  • Per-viewer playback: the tenant admin sees their org clear, an operator sees ••••
  • Shred-aware: after erasure every reference resolves to [erased]; the replay rows aren't touched
  • Inert playback: a script-free, sandboxed iframe
In the product/operator/replays/:org_id/:id/settings/replays/:id

One recording of the real CRM contacts page. Four viewers. The stored bytes never change.

An operator inside an impersonation session for Blue Ridge, the only way an operator can open a replay. Every referenced field resolves to ••••.

The same operator, holding a second-party-approved reveal grant on Ada Whitfield. Her row resolves through the vault; nobody else's does. A grant is per subject.

Blue Ridge's own admin, on the tenant plane: their org, in the clear. Typed text is still only a length; the recording never had it.

The same admin after Samen.Erasure.shred of Ada Whitfield. Her key is gone, so every reference to her resolves to [erased]. The replay rows were not touched.

/operator/replays/b1112d00…0001/bafe11ed…d9df/settings/replays/bafe11ed…d9df
Operator planeSettings/Replays/ContactsLive
Session replay
code unchanged since recording
ContactsLive recorded 2026-10-10 13:26:23 · 14 frames. Referenced fields show their current value, resolved for you now. Typed input is shape only.values are CURRENT · resolved on your plane now
render (17 changed) · at 22 ms · frame 3 / 14
Blue Ridge Logistics/CRM/Contacts
Contacts
Show archived+ New contact
Operator · operator plane · masked
Blue Ridge Logistics · tenant plane · in the clear
Contacts5· name / email / phone via PiiResolution
NameEmailPhoneCompanyTitle
·AV••••Ada Villanueva••••ada.v@laurelfork.example••••+1 555 0177▒▒▒ (18)▒▒▒ (15)
·AW••••Ada WhitfieldAda Whitfield[erased]••••ada.whitfield@blueridge.exampleada.whitfield@blueridge.example[erased]••••+1 555 0142+1 555 0142[erased]▒▒▒ (20)▒▒▒ (13)
·LH••••Lena Hartmann••••lena@shenandoahcold.example••••+1 555 0108▒▒▒ (21)▒▒▒ (5)
·MO••••Marcus Okafor••••marcus@cumberlandmills.example••••+1 555 0119▒▒▒ (16)▒▒▒ (13)
·PR••••Priya Raman••••priya.raman@blueridge.example••••+1 555 0163▒▒▒ (20)▒▒▒ (10)
References 1515 •••• masked12 •••• masked3 shown (current)15 shown (current)12 shown (current)3 erased (shredded)
Timeline · 14 frames01 mount02 navigate03 render04 event05 render06 event07 render08 event09 render10 event11 render12 event13 render14 exit

Pinch to zoom: the desktop screen, scaled to fit.

Real data, redrawn: one session recorded by the ADR-052 recorder over the real Samen.Web.CRM.ContactsLive (fictional Blue Ridge contacts) and replayed by the real player for each viewer. Labels, payloads, cell text and reference outcomes are exported by scripts/landing/replay_demo_export_test.exs; the JSON is abridged (ids shortened, some keys dropped), never altered. The player itself shows the recorded screen in a script-free sandboxed iframe.

07Cross-plane chat

One thread. Two truths.

Your support team and your customer talk in one realtime thread. Paste any object id and it unfurls into a live card, clear to the customer and •••• to support, resolved per viewer through the same two gates every page uses.

1renderer, two actors. No if operator branch exists in the card. built
  • The broadcast carries an id, never a body: every subscriber re-reads under its own actor
  • Any catalogued object unfurls the day it's catalogued, with zero card code
  • Identity masked by default; disclosing it is an opt-in, recorded act
send · Rosa Medina · tenant

Persist the message through Ash. Its body goes to the vault.

broadcast
pubsub · samen:chat:<thread_id>

{:chat_message, %{message_id, sender_party, refs}}

no body field exists on the wire

each subscriber re-reads under its own scope
tenant
OrgScopeown orgPiiResolutiontenant
Rosa MedinaConfirmed. Here is the contact and the driver on BR-4471.
operator
OrgScopevia impersonationPiiResolutionoperator
•••• blueridge-dispatch••••
In the productone thread, one message, one pair of refs, rendered per viewer's plane

Paste an object id and it unfurls into a live card: clear on one side, •••• on the other.

Blue Ridge · tenant /chat/:id
#Rate confirmation for load BR-4471cross-plane thread · owned by the tenant orgtenant
CK
SaaS support driftwood-desk · 10:24
Confirming the rate on BR-4471. Can you send the shipper contact and the assigned driver?
RM
Rosa Medina blueridge-dispatch · 10:26
Confirmed. Here is the contact and the driver on BR-4471: samen:crm.person:0f00…aa samen:freight.driver:0f00…cc
PAda Whitfieldsamen:crm.personcustomer
TitleShipping manager
Emailada.whitfield@blueridge.example
Phone+1 555 0142
your org · resolved clear (tenant plane)
DMarcus Valesamen:freight.driver · Driveron load
CDLD4471-8820
CDL stateIL
ELDsamsara
driftwood's own card for freight.driver · clear
2 online · driftwood-desk is typing…
Driftwood Ops · operator /operator/desk-chat/:id
#Rate confirmation for load BR-4471the same thread · reached through impersonationoperator · masked
·
•••• blueridge-dispatch · 10:26
••••
P••••samen:crm.personcustomer
TitleShipping manager
Email••••
Phone••••
tenant's customer · identity •••• · non-PII clear
D••••samen:freight.driver · Driveron load
CDL••••
CDL stateIL
ELDsamsara
the same card · state, ELD, status still readable
2 online · blueridge-dispatch shown as a handle only

Pinch to zoom: the desktop screen, scaled to fit.

Redrawn from Samen.Web.Chat.ThreadLive, mounted twice by driftwood: samen_chat_routes on the tenant plane and again at /operator/desk-chat with plane: :operator. The person card is the framework's; the driver card is DriftwoodWeb.Chat.DriverCard, registered as data on the mount. Message bodies are vaulted too, so the operator reads the tenant's words as ••••. Proven by chat_unfurl_masking_test.exs: the live operator DOM is scanned for leaks.

08The AI agent

The agent proposes. A human approves. The write runs as the human.

An agent near customer data is a governance problem before it's a feature. So samen's agent loop starts at authority: the agent holds no credential that can write. A write is a proposal parked for a person; on approval it executes under that person's actor, recorded as agent-originated.

0vault values, plaintext or token, that reach a model. Agent runs are masked-only on every plane. built
  • Durable, multi-step tool use with fail-honest budgets: 8 turns, 12 tool calls, 10 minutes by default
  • Grounded in the machine catalog, not guessed field names
  • mix samen.gen.agent scaffolds a new agent with its own masking proof
1 · the agentwrite credential: none
search_recordsread · personal fields come back ••••fetch_recordread · masked like any other actorassign_record_ownerwrite · calling it executes nothing
proposes
2 · the approval · ai_agent_writerun parked

Records the tool, the turn, the arg key names and a sha256 digest, never an argument value. A person who isn't the requester decides.

no decision in 24h → withdrawn, nothing executed

approve
3 · the write · runs as the approver

Executes once, inside the decision, bound to that digest. actor is the approver; requested_by is the AI principal, which holds no write authority anywhere.

the blast radius is what the approver could do by hand

In the product/ai/agents/:ida support-triage run, parked at the human gate

"Why is load BR-4471 late, and who should own it?" Two reads, one proposal, and a person decides.

Blue Ridge Logistics/AI/Agent runsRMRosa Medina
dispatcher
tenant plane
awaiting_approvaldriftwood.support_triageturn 3/8 · tool calls 3/12 · tokens 2,914/388← All runs
Awaiting your decisionThe agent PROPOSED this write. It has not run, and it cannot run without a decision from a person who is not the requester (ADR-043 §6.2).
tool:
assign_record_owner
turn:
3
arg keys:
id, resource, user_id
args digest:
9c41d07be2…e07a

Only key NAMES and a digest are recorded on the approval, never argument values (INV-1). Approving executes the write as you, once, inside the decision; the digest binds it to exactly this proposal. Deadline: 2026-10-14 10:31Z.

Approve + executeReject
Transcript

Vault-routed. An agent run is masked-only toward the model on every plane (ADR-047 §4.4); this panel resolves on YOUR plane.

Why is load BR-4471 late, and who should own it?
  1. search_records "BR-4471" → 3 hits
  2. fetch_record support.agent → full_name •••• · email ••••
  3. assign_record_owner proposed → waiting for a person
Turns
#StatusToolArg keysTokensProvider
1donesearch_recordslimit, query806/94scripted SIMULATED — not a real model
2donefetch_recordid, resource1,031/151scripted SIMULATED — not a real model
3proposedassign_record_ownerid, resource, user_id1,077/143scripted SIMULATED — not a real model

Pinch to zoom: the desktop screen, scaled to fit.

Redrawn from Samen.Web.AI.AgentLive, which driftwood inherits through one samen_ai_routes call. The agent is Driftwood.Support.TriageAgent: a name, a goal prompt and three tool kinds, nothing else. Driftwood configures no model provider, so its runs are honest about it: the deterministic Scripted provider, with a SIMULATED badge on every turn. Built and gated: ADR-047 batches A1–A7 landed.

Act III · The rest of the 80%

Everything else a B2B SaaS needs, built on the same decision.

Table stakes are where products quietly leak: the export that writes every column, the search index that holds names, the vendor SDK in the kernel. Here each one inherits the masking default instead of re-deciding it.

09Working surfaces

The table stakes, without the leaks.

CRM, billing, a support desk, work tracking, automation, files, search, CSV, settings, notifications, flags, analytics. Each ships as a scope blueprint a product mounts in one call, and each renders through the same masking default as everything else.

18scope blueprints plus 19 route macros a vertical mounts at roughly zero authored lines. built
  • Search over a non-PII index: ⌘K finds the record without the index ever holding the name
  • Files quarantined by default: promoted only after a clean scan
  • Automation with approvals: workflows compile to Reactor; approvals carry the distinct-party CHECK
10Billing & email

Delete every vendor package. The kernel still passes.

Payments and email are where frameworks quietly marry a vendor: a webhook signature here, a customer id there, until leaving costs a quarter. Samen's kernel names zero Stripe strings. Adapters live in sibling packages, and removing all of them leaves samen_core and samen_web green.

0card-number columns that can compile. Payment methods are hosted-only, and a verifier refuses any PAN-shaped column. built
  • Three email vendors that disagree: Basic-Auth, SNS-RSA and Svix-HMAC behind one conformance suite
  • Webhooks are hints, not facts: verify, fetch current state, apply idempotently, survive reordering
  • Usage metering as an insert-only ledger, with quotas summed from it
11Portfolio cockpit

Run ten products from one seat.

Samen exists to launch many products, so the operator needs one view across them: which app is healthy, which tenant is churning, which flag is rolled out where. The cockpit reads every product over a token-blind report wire, so the portfolio view never becomes the leak.

1notion of "up". The public status page is a projection of the fleet's own health, not a second opinion. built
  • Tenant names masked by default, unmasked per role down to account level: a salesperson sees their accounts
  • Fleet directives push flags across products; an unreachable app degrades honestly, never a fake "applied"
  • Explainable health scores and an MRR-movement ledger per tenant
Act IV · Proof

Every claim above is attached to something that can go red.

A guarantee nobody has tried to break is a decoration. So samen tries, on every pull request, and keeps the attempts.

12Sabotage & mutation

A test that can't fail is a bug.

Each guarantee ships with a sabotage: a committed patch that breaks exactly that behaviour. The harness applies it, demands that the named tests fail (not just that something breaks), reverts, and checks the tree is restored byte for byte. Then a mutation gate asks the converse: what break did nobody think to sabotage?

604committed sabotages, replayed on every PR where they touch the diff and in full every night. counted from the tree
  • 3,960 mutants across 228 targets, enumerated from the AST, each run against only its owning tests
  • A survivor fails the build until a test kills it or a ledger row proves it can't matter
  • 25 verifiers check structure the tests can't: no PII columns, catalog parity, fleet wire…
$ scripts/sabotage.sh --range 187-187187-pp13-reveal-approver-distinct-party-…patch APP samen_core · TEST_FILES test/reveal_approver_test.exs MUST_FAIL "a self-approval is refused with :self_approval" 1 preflight tree clean, header linted2 apply disable both policy-layer self-approval checks3 run ✗ a self-approval is refused with :self_approval4 revert git apply -R5 restore sha-256 byte-exact ✓ FLIPPED the named test went red, then green again $ ./scripts/mutate.sh --file samen_core/lib/samen/approvals/gate.ex EQ · REL · BOOLOP · BOOLLIT mutants, owning tests onlyMUTATION GATE: ALL PASSED

Sabotage 187 is real: the database CHECK still backstops the write, but the clean policy refusal degrades, and the named test catches it. Both layers are load-bearing.

13CI for agents

Verdicts, not logs.

This codebase is written by AI agents, so its CI is built for them: cheap, fast and mechanical. quick during a round, pr before a pull request, full nightly. Every call fits a single tool call, ends in one verdict line, and resumes where it stopped.

0.4seconds for quick when nothing changed, because every step is cached by the content it reads. A failure is never cached. measured
  • A flaky test still fails the run: fix the flake, don't retry it green
  • Mirrored on GitHub Actions: the PR check verifies every step ran once, on this commit
  • The old ci.sh took about 13 minutes and never fit one tool call; agents cut it up by hand
$ scripts/ci quick CACHED dialyzer_samen_core PASS samen_core 151.2sCI(quick): PASS 19/19 in 197.5s — NOT PR-READY — run: scripts/ci pr $ scripts/ci pr CI(pr): budget — next step sabotage_changed[3/5] would overrunCI(pr): INCOMPLETE 49/52 — continue: scripts/ci resume $ scripts/ci resume PASS sabotage_changed[4/5] 226.6s PASS sabotage_changed[5/5] 211.7sCI(pr): PASS 52/52 in 212.3s — PR-READY

Lines from real runs on this repo, trimmed. A red step prints a digest and its log path; the verdict also lands in _ci/last.json.

—Scope, stated on purpose

What this is not, stated as precisely as what it is.

A project that can draw its own boundary is one whose other claims you can price. Each limit below is written in the repo in the same words. When a reviewer finds a claim that outruns its evidence, the fix is a commit, and the retraction stays visible.

01 · Keyless lane

No live vendor is wired in CI.

Billing and email are proven against hermetic fakes and recorded transports through a real conformance suite, never a real account. Generated apps boot with both unconfigured and say so. The seams are proven; the wires are yours.

02 · Support access

Support can see PII.

Through a grant a second person approved, that expires on its own and that the tenant can audit. "Support never sees plaintext" was once on this page; it was false by construction and was retired.

03 · Verticals

The verticals are proof, not product.

driftwood (freight), pawchart (veterinary) and demo exist so the substrate must survive unrelated domains. They aren't for sale. If a vertical does something clever, that's a bug in the kernel.

04 · Leverage

188 lines is a mount number, not an app size.

pawchart mounts framework surfaces in about 188 authored lines; pawchart itself is about 3,900. An earlier page conflated the two, and the overclaim was corrected rather than reworded.

05 · Residency & distribution

US-only, and not on Hex yet.

One Postgres per host, no per-tenant region selection. The framework ships as path dependencies inside this monorepo; publishing waits on a stated trigger.

06 · Erasure's edge

Erasure can't reach what samen never wrote.

A spreadsheet someone exported, or a downstream system that stored its own plaintext, is outside the boundary. The page won't pretend otherwise.

—Two ways in

Run it in ten minutes, or read it for an afternoon.

Path A · Use it

Generate an app and make it prove itself.

You need Elixir 1.20 on OTP 29 and a local PostgreSQL with pgvector. No Docker, no API keys, no accounts.

$ git clone https://github.com/ckluis/samen && cd samen/samen_core$ mix samen.gen.app --module Harbor --prefix hb --abbrev hrb$ cd ../harbor && MIX_ENV=test bash ci.sh19/19 steps ALL PASSED # zero hand-edits

If the generated app needs a fix from you to go green, the generator is broken, not your setup.