Field notes

The primitive is a post-auth trust decision

Thesis

A technical field note on the deterministic decision AfterAuth makes after identity and before costly AI usage.

The word "primitive" is everywhere in AI right now.

Security primitives. Scheduling primitives. Tool-use primitives. Agent-runtime primitives. The useful version of that word means: a small, stable thing you can compose into many workflows without dragging the whole product surface along with it.

That is the category AfterAuth is trying to earn, but the claim has to be narrow enough to be useful.

AfterAuth is not building the primitive for access control in general. That would be too broad, and it would collapse into authz, RBAC, feature flags, gateways, OPA, billing entitlements, and half the cloud security market.

The primitive is smaller: a post-auth trust decision.

An authenticated user asks for something costly. The product sends the requested action, current trust signals, prior events, and policy context. AfterAuth returns a deterministic decision before spend happens: allow, limit, delay, require verification, or deny, with a receipt.

The primitive

Auth answers who got in. AfterAuth answers what they get next, under policy, with a receipt.

This is the missing primitive for products where activation now has real marginal cost. A no-card trial used to mean a user could click around an empty workspace. In an AI product, a no-card trial might mean inference credits, image generation, long-running agents, hosted environments, data enrichment, exports, webhooks, scraping, queue work, or support attention.

That changes the old bargain.

You no longer have to choose between a card gate that forces conversion before the user has seen enough value and an open-ended freemium surface that lets every new account burn through expensive resources. There is a third path: make a post-auth trust decision, start with bounded access, unlock more as trust improves, and keep the outcome explainable.

The existing primitives do not compose into this cleanly

Most teams already have pieces of the answer.

Auth providers identify the user. Feature flags route code paths. Rate limiters protect hot endpoints. Billing meters count usage. Analytics records behavior. Fraud tools detect suspicious patterns. OPA and similar systems evaluate policies. A database table can hold quotas. Support can keep notes. Finance can ask for a report.

All of those are real tools. None of them are made obsolete by AfterAuth.

The problem is that the after-auth decision usually sprawls across them:

  • a feature flag decides whether the button appears;
  • product code checks a quota column;
  • a billing meter records the usage after it happened;
  • a rate limiter catches volume, not trust;
  • support has no plain answer for why this account got blocked;
  • finance sees cost, but not the policy that authorized it;
  • product wants to change the trust ladder without another deploy.

The workflow exists, but it does not have a first-class object.

AfterAuth's bet is that the object should be a post-auth trust decision.

What the primitive has to do

A post-auth trust decision has a smaller contract than a ledger and a larger contract than a feature flag.

It needs to run in the critical path before costly work:

ts
const decision = await afterauth.evaluate({
  protectedActionKey: "research_agent_run",
  userId: user.id,
  requestedAmount: 1,
  signals: {
    email: user.email,
    emailVerified: user.emailVerified,
    accountAgeDays: user.accountAgeDays,
    hasPaymentMethod: false,
  },
  metadata: {
    workspaceId: workspace.id,
    surface: "activation",
  },
})

if (decision.outcome === "deny") {
  return showTrustMilestone(decision.reasonCodes)
}

if (decision.outcome === "allow_limited") {
  await runResearchAgent({ maxRuns: decision.approvedAmount })
}

Then, after the expensive work succeeds, the application reports the event that future decisions can count:

ts
await afterauth.reportCompletion({
  action: "research_agent_run",
  userId: user.id,
  requestId: decision.requestId,
  decisionId: decision.id,
  metadata: {
    completed: true,
    outputCount: 1,
  },
})

That flow is intentionally boring. The product asks before spend. The product reports after completion. AfterAuth keeps the policy-backed receipt and the append-only facts needed to decide the next request.

The primitive has to answer five questions every time:

  1. What costly action is being requested?
  2. What does this product currently know about the user?
  3. What prior events or unlocks has this user earned?
  4. Which policy rule matched, if any?
  5. What exact access should the product grant now?

The output cannot be vibes. It has to be a typed decision:

ts
type DecisionOutcome =
  | "allow"
  | "allow_limited"
  | "delay"
  | "require_verification"
  | "deny"

The useful part is not just the enum. It is the frozen context around it: requested amount, approved amount, protected action, prior events, matched rule, reason codes, policy id, allowed capabilities, runtime period state, and evidence hash.

That is what turns "we blocked them somehow" into "this account asked for a long-running research agent, had not connected a work signal yet, had already used the starter run, matched the default deny policy, and was shown the next trust milestone."

The trust loop

The model is simple enough to draw.

AfterAuth trust loopThe product asks before spend, reports after completion, and the next request sees the updated facts.
flowchart LR
A["Authenticated user"] --> B["Request protected action"]
B --> C["AfterAuth evaluate"]
C --> D{"Policy decision"}
D -->|allow| E["Run costly work"]
D -->|allow_limited| F["Run bounded work"]
D -->|delay or verify| G["Ask for trust milestone"]
D -->|deny| H["Block before spend"]
E --> I["Report completion event"]
F --> I
G --> J["Report milestone event"]
I --> K["Append-only facts"]
J --> K
K --> C

This is not an authorization check in the usual sense. The user is already authenticated. The question is not "can this user ever access this route?" The question is "how much costly value has this user earned right now?"

That means the answer can be more expressive than yes or no:

  • let them run one research agent, not ten;
  • let them generate sandbox assets, not production exports;
  • allow image generation until five completions exist this month;
  • unlock a larger cap after work email verification;
  • require review for ambiguous or high-cost actions;
  • deny disposable, repeated, or velocity-flagged patterns before spend.

That is why the post-auth trust decision is the primitive. Controlled, gated access is what it enables. The access is not static. It changes as trust changes.

Why this is not a full ledger

It is tempting to turn every unlock into a balance.

Work email verified: add five credits. GitHub connected: add three. Support retry: add one. Failed run: refund one. Finance report: reconcile every credit source.

That is a real architecture. Sometimes it is the right one.

But a ledger is a bigger promise. Once AfterAuth says it owns balances, it has to own grant buckets, transaction ordering, reservations, refunds, expiry, revocation, support adjustments, concurrency, and reconciliation. Those are solvable problems, but they are not the first pain point.

The first pain point is earlier:

Should this unproven but authenticated user be allowed to trigger this costly action right now?

For that, immutable post-auth decisions plus append-only events are enough to carry a lot of weight.

A policy can say: allow studio_generation while this user has fewer than five studio_generation_completed events. Another rule can say: allow research_agent_run once after GitHub is connected. Another can say: deny by default unless a trust milestone exists.

No mutable balance is required for that model. The current state is derived from durable facts.

Decision model, not balance modelAfterAuth can gate costly actions from policy and events without becoming the system of record for every credit bucket.
flowchart TB
subgraph Inputs["Inputs"]
I1["Protected action"]
I2["Trust signals"]
I3["Prior events"]
I4["Active policy"]
end

subgraph Core["Deterministic core"]
C1["Ordered rules"]
C2["Abuse checks"]
C3["Event-count caps"]
C4["Approved amount"]
end

subgraph Outputs["Outputs"]
O1["Decision"]
O2["Reason codes"]
O3["Allowed capabilities"]
O4["Frozen receipt"]
end

Inputs --> Core --> Outputs

The boundary matters.

AfterAuth should leave billing systems, subscription entitlements, and mature ledgers alone when they are already the right source of truth. It should sit in front of costly activation and answer the pre-spend trust question. If a product later needs named grant buckets and financial reconciliation, the post-auth decision receipt can become an input to that ledger. It does not need to pretend to be the ledger on day one.

Where LLMs fit

This is the part I think gets more important as AI tooling spreads through product teams.

LLMs are very good at making product surfaces cheaper to create. They can scaffold an admin UI, write a first policy editor, draft docs, suggest onboarding flows, and help a founder explore what a trust ladder should look like.

They should not be the runtime authority deciding whether a stranger gets to spend your model budget.

The right shape is an LLM-assisted authoring experience wrapped in a deterministic harness.

In AfterAuth terms, that means the AI can help a user describe the policy:

text
Give new accounts one image research run.
Give work-email users five studio generations.
Give GitHub-connected users eight generations.
Block repeated disposable-email patterns before spend.
Show support the reason code.

But the runtime artifact has to compile down to explicit rules:

json
{
  "defaultOutcome": "deny",
  "defaultApprovedRatio": 0,
  "rules": [
    {
      "ruleKey": "github_connected",
      "position": 0,
      "conditions": {
        "requestMatch": {
          "requestType": "studio_generation",
          "benefitType": "team_content_generation"
        },
        "hasPriorEvent": "github_connected",
        "priorEventCount": {
          "eventType": "studio_generation_completed",
          "lt": 8,
          "scope": "user"
        }
      },
      "outcome": "allow",
      "approvedRatio": 1,
      "maxApprovedAmount": 4,
      "allowedCapabilities": ["studio_production_generation"],
      "reasonCode": "github_verified"
    }
  ]
}

The LLM can suggest. The human can edit. The simulator can show what would happen. The deployed evaluator stays deterministic.

That is the product direction I care about: make it easier for teams to use LLMs to express and test trust policies, without letting an LLM become the unbounded decision-maker inside the spend path.

There are several practical features implied by that:

  • starter policies for common AI products, such as generation credits, research agents, exports, automations, and API enrichment;
  • protected-action setup that turns "image generation" or "agent run" into a stable runtime key;
  • simulation over sample users and event histories before a policy goes live;
  • receipts that show the matched rule, cap, remaining access, and next trust milestone;
  • SDK helpers that pair evaluate with completion events so teams do not forget the second half of the loop;
  • policy editing that can be assisted by AI but saved as typed, inspectable rules.

That is not magic. It is the opposite of magic. It is deliberately plain machinery around a high-risk moment.

What changes for product-led growth

The old PLG default was generous access plus a hope that the economics worked out.

The defensive alternative was a card gate, a short clock, or a tiny trial that forced the user to convert before they had learned enough. That protects margin, but it also punishes the good users who need a little more proof.

Trust-based unlocks let the product say:

  • "You can try the expensive thing, but only in a bounded way."
  • "Connect a stronger signal and the cap increases."
  • "Complete the first real workflow and the next one opens."
  • "This request needs review because the cost or pattern is unusual."
  • "This account is blocked before spend, and here is the policy reason."

That is a more humane growth motion for the real user and a more controlled cost model for the company.

The important thing is that the post-auth trust decision is reproducible. Support should not need to reverse-engineer five systems. Finance should not need to guess why a no-card account received value. Product should not need to deploy code to change the trust ladder. Engineering should not need to keep sprinkling if statements around the codebase every time the business learns something new.

The primitive test

A real primitive has to pass a few tests.

It has to be small. AfterAuth should not require a company to move auth, billing, analytics, or fraud tooling into a new universe.

It has to be composable. The trust decision should be callable from signup, first activation, generation, export, automation, support retry, referral reward, or any other costly post-login moment.

It has to be deterministic. The same request, facts, and policy should produce the same decision and evidence hash.

It has to be explainable. A user-facing block, a support answer, a product experiment, and a finance review should all point back to the same receipt.

It has to respect boundaries. A team with a mature internal policy engine may only need ideas from this model. A team with a cheap free tier may not need it yet. A team that needs financial-grade balances may need a ledger. AfterAuth should be useful without pretending every product needs the full surface.

That is the claim: the post-auth trust decision deserves to be a primitive.

The market already has primitives for getting users in, charging them, metering them, flagging them, and analyzing them. The awkward gap is the moment after identity and before costly value.

That is where AfterAuth lives.

AI use disclosure

Reviewed before publishing

Mode
Assisted
Model
GPT-5 Codex (Codex)
Source
Founder notes from this drafting session, AfterAuth public-post workflow, local product plans, public code, existing posts, SDK/core types, demo integration, and prelaunch copy
AI handled
Technical structure, objection handling, draft synthesis, deterministic-harness framing, diagram authoring, and publishing checklist
Human held
Thesis, strategic language, product direction, examples, factual review, final edits, and approval before publishing
Boundary
No customer data, private financial data, secrets, invented metrics, invented incidents, or invented customer stories used