Vibe code your own Postmark
Vibe code your own Postmark as one template, a send route, and a delivery log. Bounce lists, dedicated IPs, and reputation are the long row.
You can vibe code your own Postmark as one template, a send route, and a delivery log. Postmark is that product. The catalog line is “Save a template, send it, and read the log.”
The card prices Basic at $15 a month through Platform at $18, each starting at 10,000 emails. Pro is $16.50 a month at that volume. Extra email is $1.80, $1.30, or $1.20 per thousand, depending on the plan. The free 100-email plan and add-ons are outside that note. The first estimate is 10 hours.
Last time I wrote about building a ConfigCat alternative. That was a regular backlog post. This one is a teardown.
Vibe code your own Postmark as a send log
The Postmark page splits the work into three rows.
The first row is 10 hours. One template, a send route, a delivery log. You save a subject and a text body with named variables. A route accepts the template name, the recipient, and the variables. The log moves from queued to sent or failed.
The second row is 3 days. Retries, a second token, and a test send. A failed relay updates the same log row. You can mint a token, see it once, and revoke it.
The third row is a month and up. Dedicated IPs, reputation, and inbound mail. Skip that row.
If you tell an agent to “clone Postmark”, it aims at the third row. Copy the prompt, then pin it to the first one.
Store Template, Variable, Send, and DeliveryAttempt. The card also names ApiToken. A single server token in the environment is enough for the 10-hour cut. The token screen arrives with the 3-day row.
A template has a name, a subject, and a text body. A variable is a name the body expects. A send is one recipient, one template, and a state. A delivery attempt hangs off that send. A retry updates the send. It does not insert a second row for the same request id. That rule is on the card.
The audience is a developer sending receipts and password resets from one app. The verdict is “The log is the product”. A campaign list is Mailchimp at $20–$350 a month, in Build a Mailchimp alternative. A broadcast is Kit at $29–$79 a month, in Build a Kit alternative. One triggered email is Customer.io at $100–$1,000 a month. Those call a relay. This build is the log in front of an SMTP account you already have.
The model you pass on send is small. The template names the keys. A missing key refuses the send, which is the catalog rule, so a receipt never goes out half blank.
{
"name": "Ada",
"total": "18.00"
}
How does Postmark work
Postmark publishes the model in the email API, the templates API, the message streams API, the bounce API, the suppressions API, and the webhooks overview. I am summarizing those pages. Where they stop, I call it a guess.
A server is the boundary. Each server has its own API token, sent as X-Postmark-Server-Token. A missing or wrong token is a 401. The email API says that split isolates access and data per application.
A message stream sits on that server. Types are transactional, broadcast, and inbound. A server may have up to 10 streams, including the defaults, and only one inbound stream. The default transactional id is outbound. If you omit MessageStream on send, the message uses that outbound stream. Broadcast streams require unsubscribe handling. Transactional streams default to none. Keeping a newsletter off the password-reset stream is the split those rules describe. The numeric reputation score on their side is not on that page. Treat the score as a guess.
Templates live on the server, up to 100. Send with TemplateId or TemplateAlias, plus a TemplateModel object. The API applies that model to the subject, the HTML body, and the text body. From has to be a confirmed sender signature. The call returns a MessageID. Webhooks quote that id later, which is how a delivery lines up with your log row.
Webhooks are per stream. You can enable delivery, bounce, spam complaint, open, click, and subscription change. On create, Postmark calls your URL for each enabled type and expects a 200. If one type then fails every attempt, they keep trying, and after 48 hours they disable that type until you verify it again. A broken bounce hook leaves delivery running. Answer 200 after you store the event, and ignore a repeat MessageID.
Bounces are kept for 45 days by default. A hard bounce is code 1, the mailbox is missing. A soft bounce is code 4096, the mailbox is full or over quota. Inactive means that bounce deactivated the address. CanActivate says whether you may turn it back on. Bounce dumps older than 30 days are dropped.
Suppressions are per stream. A suppressed address is not active for sending. Reasons they list are HardBounce, SpamComplaint, and ManualSuppression. Deleting a hard-bounce suppression reactivates that bounce. Spam-complaint suppressions cannot be deleted. One call accepts up to 50 addresses. Postmark then refuses the address. A 10-hour log will retry it until you store a stop list of your own.
The Domains API lists calls to verify DKIM, a Return-Path, and an SPF record. I have not walked the DNS shapes here. The catalog’s month row names dedicated IPs beside that work, and the pricing note has no IP add-on price.
Inbound is a separate stream, with its own parse webhook. The card’s non-goal leaves that out of v1. A shared inbox is Help Scout, covered in Vibe code your own Help Scout.
Node.js plus Astro is the default for the log and the templates. Use TanStack Start if React is your weekday language. The card’s stack is Astro, Workers, and D1. The send route can be a Worker so a receipt does not depend on the admin page staying open. D1 holds templates and the log because those rows are yours. The SMTP password and the API token stay in the Worker config. The guardrail says to redact message bodies from the log and from error text.
Screens of a Postmark alternative
v1 needs the template, the log, and a way to send a test. Tokens can be a config value until the 3-day row.
Template
The template screen is a name, a subject, a text body, and the variable names you found in that body. Save rejects a body that names a variable you did not list. A test send asks for a recipient and a value per variable, then writes a normal log row.
Delivery log
The log is recipient, template, state, and the provider response with secrets removed. Queued, sent, and failed are the states on the card. A retry button shows on failed. After a successful retry the row says sent, and a second row does not appear.
The retry control on that wireframe is the 3-day behavior. For the 10-hour cut, a failure stays failed and you send again as a new request only when you mean to. The card’s second row is the one that makes retry safe.
How a Postmark alternative sends mail
You save the receipt template and send a test to yourself. The log row is queued, then sent, and the message arrives with Ada’s name filled in.
The app calls the route with that template, Ada’s address, and the model. The route checks the token, checks the variable names, writes the send, and hands the rendered text to the relay. The log moves to sent when the relay accepts it.
The relay returns an error. The row moves to failed. The body is not copied into the error. On the 3-day row you press retry, the same row moves to sent, and Ada does not get a second receipt from a duplicate insert. A revoked token gets an error and writes no send row. That scenario is on the card.
The hard half of a Postmark alternative
The skip row starts when the log has to decide whether an address may be mailed again, and when the mail has to leave from an IP other servers trust.
Suppression is that decision. It is stored per stream, hard bounces come off if you delete them, and spam complaints stay. A password reset and a newsletter use different streams so one audience’s complaints stay off the other stream. Your v1 has one relay and one log. It retries a dead address until you copy a slice of that list.
Bounce codes are a taxonomy. Code 1 and code 4096 want different next steps. The public table also includes DNS errors, DMARC policy, ISP blocks, and a template rendering failure. Rebuilding that classifier means reading raw delivery reports. The 10-hour row stores the relay’s error string, redacted, and stops.
Dedicated IPs are the catalog’s month row. The pricing note has no add-on price for one, so that number stays on their pricing page. Inbound parsing is a second webhook with its own failures. Leave both alone.
Open and click tracking are on their webhook list. The 10-hour scope leaves them out.
The non-goal on the card says not to run mail servers or warm up IPs. Mailchimp already covers a campaign. Kit already covers a broadcast. Customer.io already covers one triggered email.
Start with the first row
Go to Build your own Postmark. Copy the Astro prompt, or TanStack Start if that is what you write.
Tell the agent the first estimate only. One template, a send route, a delivery log. Ten hours.
If you still need someone else to accept the mail and refuse a dead address, keep the $15 plan and point the route at it. The log you own is the part you were rebuilding inside every app anyway.