The path a message takes
Running a support inbox is the walkthrough; the reference has every field; step 6 is classification.
- Mail arrives at your app Worker’s single email entry, which fans it to every capability that handles inbound mail.
- Support claims what is addressed to it — on the SMTP envelope recipient, never a
To:header. - The guard bounds it, and provenance decides whether the sender is who they claim.
- It is parsed — multipart and attachments included.
- The HTML is sanitized through the runtime’s own parser.
- The raw MIME goes to R2 unchanged.
- The row lands in D1.
- Only then is classification dispatched, as a Workflow.
Writing in from inside the app
POST /support/feedbackSession and same-origin, and nothing else, permanently.
It opens a thread, or appends to one the caller already owns — same table, same classifier, same taxonomy, same console.
There is no second inbox, because a support person with two consoles has the whole conversation in neither.
Where your own authorization goes instead
If your account model makes some submissions act-on-behalf-of — an application made for an organization, which a member may not make — put that check in your app capability’s middleware, over your own path:
app.use("/support/feedback", async (c, next) => {
// Signed out? Pass it through — the route's own gate answers 401. A 403 here would tell
// somebody who was never signed in that they are forbidden.
if (c.var.auth && !(await mayActForTheOrganization(c))) {
throw new ForbiddenError({ message: "An owner or an admin applies on the organization's behalf." });
}
await next();
});Every capability’s middleware mounts before any capability’s routes, and your app composes last — so yours runs after auth has resolved the session and before this capability’s own gate.
Write the path from the base path you configured. A mount point you moved and a middleware path you did not is a gate that silently stops covering anything.
The context the app supplies
Screen, build, platform, environment, locale — what the user should not have to type.
That set is closed. An undeclared key is refused rather than stored, because the risk of an open bag is an adopter passing their whole client state through it and quietly landing a customer’s data in an inbox a console renders.
Bounds on a public write endpoint
Because that is what a public address is:
guard: {
maxRawBytes: 2_000_000,
maxPerSenderPerHour: 20,
maxPerHour: 500,
archiveSpam: true,
}archiveSpam is what makes the spam category do something rather than just label something.
Replying
POST /support/threads/:id/reply — control-plane, so it is an issued credential rather than a staff member’s session.
A mailed reply to an app thread is recorded with channel: "email", and the thread’s own message id is what makes the customer’s answer land back on the same thread.
What you own
The mail, the database, the bucket, and the inference.
There is no Pithy-operated service in the path, and nothing here that a hosted dashboard unlocks — every route is reachable with a credential you issued, whatever anyone pays.