{"version":"https://jsonfeed.org/version/1.1","title":"One Shot blog","home_page_url":"https://shiponeshot.com/blog","feed_url":"https://shiponeshot.com/feed.json","description":"Everything your app needs, already built in: accounts, payments, email, and mobile app shells. Hand it to your coding agent and build the part that's yours.","language":"en","items":[{"id":"https://shiponeshot.com/blog/how-long-it-takes-to-build","url":"https://shiponeshot.com/blog/how-long-it-takes-to-build","title":"How long does it take to build the parts every app needs?","summary":"Measured against a real codebase: what sign-in, payments, tenancy, email, mobile, and deployment actually contain, and why every estimate for them comes in low.","content_html":"<p>How long does it take to build the parts every app needs?</p>\n\n<p>For a competent developer starting from an empty directory, <strong>21 to 43 working days</strong> before a\ncustomer can do anything useful. That covers only the core: sign-in, payments, keeping customers\nseparate, email, background jobs, hardening, and deployment. Add mobile apps and the pages that make\nyou findable and it is closer to <strong>34 to 68 days</strong>. This post checks those numbers against a real\ncodebase, area by area.</p>\n\n<h2 id=\"how-these-numbers-were-measured\">How these numbers were measured</h2>\n\n<p>Every figure below comes from this codebase, counted with <code>wc -l</code> on the working tree and\n<code>bin/rspec --dry-run</code> for the test count. Nothing is estimated after the fact.</p>\n\n<p>Two caveats, stated up front. <strong>Line counts include comments</strong>, and this codebase comments heavily.\nAnd <strong>lines are a weak proxy for effort</strong>: one hard twenty-line method can cost more than two\nhundred easy ones. The day ranges are judgement, not measurement. The line counts are the evidence.</p>\n\n<p>So read this as the shape of the work rather than a stopwatch: which parts are large, which are\nsmall, and which are nothing like the size people assume.</p>\n\n<h2 id=\"sign-in-sounds-like-two-days\">Sign-in sounds like two days</h2>\n\n<p>Passwordless sign-in with Google and Apple sounds like an afternoon and a gem. The repository holds\n<strong>705 lines of application code across 19 files</strong>, and <strong>377 lines of tests running 39 examples</strong>.</p>\n\n<p>Here is where the rest of the time goes:</p>\n\n<ul>\n<li>Codes expire, at ten minutes.</li>\n<li>Codes are single use, and failed attempts are capped at five.</li>\n<li>The code is never stored in plain text, only as a keyed digest, so a database leak reveals no live\ncodes.</li>\n<li>Requests are throttled by IP address <strong>and</strong> by email address. Only one of those stops a targeted\nattack.</li>\n<li>Apple returns its callback as a cross-site form post, so the route must accept both GET and POST,\nand the client secret is a token you build and sign yourself.</li>\n<li>The same person can arrive as a magic code today and a Google account tomorrow. Deciding those are\none human is a schema decision you make once and live with.</li>\n</ul>\n\n<p>None of that is optional, and all of it is invisible in a demo.</p>\n\n<h2 id=\"payments-is-not-the-checkout\">Payments is not the checkout</h2>\n\n<p>The checkout is perhaps a fifth of it. The subscription stack is <strong>572 lines</strong>, <strong>225 of them the\nwebhook alone</strong>, against <strong>864 lines of tests running 80 examples</strong>. The tests outweigh the code,\nwhich is the correct ratio for money.</p>\n\n<p>What the webhook has to survive:</p>\n\n<ul>\n<li>Signature verification, with a clean rejection for a malformed payload rather than a swallowed\nsuccess.</li>\n<li>Duplicate delivery. The same event arriving twice must change nothing the second time.</li>\n<li>Never returning a server error, because providers disable endpoints that keep failing.</li>\n<li>Refunds, disputes, and fraud warnings, each of which has to pull access back.</li>\n<li>Cancellations that take effect at the end of the paid period, not immediately.</li>\n<li>Storing a status and a period end, not a boolean, because \"paid\" is not a yes or no question.</li>\n</ul>\n\n<p>The gate that actually checks all of this before someone uses a paid feature is <strong>21 lines</strong>. That\nratio is the whole point: the enforcement is trivial, and the bookkeeping behind it is not. There is\nmore on why in <a href=\"/blog/charging-for-your-app\" rel=\"noopener\">how to charge without building billing</a>.</p>\n\n<h2 id=\"keeping-customers-separate-is-cheap-in-code\">Keeping customers separate is cheap in code</h2>\n\n<p>This is the smallest area on the list and the most expensive to get wrong: <strong>275 lines</strong>, of which\nonly <strong>64 are permission policy</strong>, plus <strong>176 lines of tests</strong>.</p>\n\n<p>It is cheap because the decision is made once, at the schema, and then followed everywhere. It is\nexpensive because there is no second chance. The test that matters asserts that another account's\nrecord returns a 404, and it is worth more than the rest of the area combined. The full argument is\nin <a href=\"/blog/keeping-customer-data-separate\" rel=\"noopener\">how to keep one customer from seeing another's data</a>.</p>\n\n<h2 id=\"two-areas-that-really-are-small\">Two areas that really are small</h2>\n\n<p>Email is <strong>120 lines</strong>, about ten of them the production mail settings, and there is no library. But\nthe days go to DNS records and to finding out a week later that your mail lands in spam because one\nrecord was missing. That work shows up in no line count at all, which is exactly why estimates miss\nit.</p>\n\n<p>Background jobs are <strong>145 lines</strong> including configuration, with <strong>104 lines of tests</strong>. No Redis and\nno extra service to run or pay for, because the queue lives in the database you already have. This\nis the one area where the honest answer is that it simply got much easier.</p>\n\n<h2 id=\"hardening-is-a-list-not-a-feature\">Hardening is a list, not a feature</h2>\n\n<p><strong>395 lines</strong> against <strong>383 lines of tests running 36 examples</strong>. It is not one thing. It is six\nrate limits, an enforced content policy, request filtering, and the export and deletion routes the\nlaw expects you to have.</p>\n\n<p>The content policy deserves a warning. It is enforced rather than reporting, so a missing allowance\nbreaks a real flow <strong>silently</strong>: no error, no failed request, just a button that does nothing. Ten\nof those 36 examples exist only to pin that down.</p>\n\n<h2 id=\"mobile-is-the-one-people-underestimate\">Mobile is the one people underestimate</h2>\n\n<p><strong>802 lines of native code</strong> and <strong>743 lines on the server</strong>, plus <strong>525 lines of tests running 46\nexamples</strong>.</p>\n\n<p>The shells themselves are small. The cost is that both stores require their own purchase system for\ndigital goods, so you now have a second payment path with its own receipts and notifications, and\nboth paths have to arrive at one answer about who is subscribed. See <a href=\"/blog/your-app-in-the-app-stores\" rel=\"noopener\">getting your app into the app\nstores</a>.</p>\n\n<h2 id=\"being-findable-is-the-largest-area-here\">Being findable is the largest area here</h2>\n\n<p><strong>1,552 lines</strong>, <strong>118 examples</strong>. It is the biggest single area in this repository, and it is the\none nobody puts in an estimate at all.</p>\n\n<p>That is the blog, the feeds, the sitemap, the structured data, the machine-readable files AI\nassistants read, and generated share cards. None of it is hard. There is simply a lot of it, and\nnone of it exists until you write it.</p>\n\n<h2 id=\"the-whole-thing-added-up\">The whole thing, added up</h2>\n\n<table><thead>\n<tr>\n<th>Area</th>\n<th>Code</th>\n<th>Tests</th>\n<th>Realistic days</th>\n</tr>\n</thead><tbody>\n<tr>\n<td>Sign-in</td>\n<td>705</td>\n<td>377</td>\n<td>5 to 10</td>\n</tr>\n<tr>\n<td>Payments</td>\n<td>572</td>\n<td>864</td>\n<td>5 to 10</td>\n</tr>\n<tr>\n<td>Keeping customers separate</td>\n<td>275</td>\n<td>176</td>\n<td>3 to 6</td>\n</tr>\n<tr>\n<td>Email</td>\n<td>120</td>\n<td>47</td>\n<td>2 to 4</td>\n</tr>\n<tr>\n<td>Background jobs</td>\n<td>145</td>\n<td>104</td>\n<td>1 to 3</td>\n</tr>\n<tr>\n<td>Hardening</td>\n<td>395</td>\n<td>383</td>\n<td>3 to 5</td>\n</tr>\n<tr>\n<td>Deployment and CI</td>\n<td>686</td>\n<td>n/a</td>\n<td>2 to 5</td>\n</tr>\n<tr>\n<td><strong>Core subtotal</strong></td>\n<td><strong>2,898</strong></td>\n<td><strong>1,951</strong></td>\n<td><strong>21 to 43</strong></td>\n</tr>\n<tr>\n<td>Mobile shells and in-app purchase</td>\n<td>1,545</td>\n<td>525</td>\n<td>8 to 15</td>\n</tr>\n<tr>\n<td>Blog, SEO, and share cards</td>\n<td>1,552</td>\n<td>1,005</td>\n<td>5 to 10</td>\n</tr>\n<tr>\n<td><strong>Total</strong></td>\n<td><strong>5,995</strong></td>\n<td><strong>3,481</strong></td>\n<td><strong>34 to 68</strong></td>\n</tr>\n</tbody></table>\n\n<p>Repository-wide: <strong>5,949 lines of application code, 3,595 lines of tests, 327 examples, 12\nmigrations, 50 routes</strong>. The two totals are close but not identical: the table adds native and\nconfiguration code that lives outside the application directory, and leaves out application code\nbelonging to none of these areas.</p>\n\n<p>Deployment is the strange row. It contains no application code at all. It is 686 lines of\nconfiguration, and it still takes most of a week the first time.</p>\n\n<h2 id=\"why-every-estimate-comes-in-low\">Why every estimate comes in low</h2>\n\n<p>Every feature on that list has two versions.</p>\n\n<p>The demo version works. Sign-in signs you in, the checkout charges a card, the email sends. That\nversion genuinely is an afternoon, and it is the version people picture when they give a number.</p>\n\n<p>The done version is the bulleted lists above: expiry, replay, throttling, duplicate webhooks,\nrefunds, the silent policy failure, the second payment path. Not one of those is optional once real\npeople are using the thing, and none is visible until they are.</p>\n\n<p>The gap between the two versions is the estimate error. It is not a fudge factor, it is a specific\nlist, and it is roughly the same list in every product anyone builds. A coding agent compresses that\nlist meaningfully. It does not shorten it. That is the argument in <a href=\"/blog/what-your-agent-should-not-build\" rel=\"noopener\">what your coding agent should\nnever build from scratch</a>.</p>\n\n<h2 id=\"the-part-nobody-estimates-at-all\">The part nobody estimates at all</h2>\n\n<p>The tests. <strong>3,595 lines against 5,949 lines of application code</strong>: six lines of test for every ten\nof application. If an estimate did not include that, it was not an estimate for finished work. It\nwas an estimate for the demo version.</p>\n\n<h2 id=\"the-short-version\">The short version</h2>\n\n<ul>\n<li>The core is <strong>21 to 43 days</strong>. Everything, including mobile and the content surfaces, is <strong>34 to\n68</strong>.</li>\n<li>Payments and sign-in are where the tests should outweigh the code.</li>\n<li>Being findable is the largest single area, and the one always left out.</li>\n<li><strong>Roughly none of it is your product.</strong></li>\n</ul>\n\n<p>The money side of the same question is in <a href=\"/blog/what-it-costs-to-start\" rel=\"noopener\">what it actually costs to start an\napp</a>, and the full inventory of what is already built is in\n<a href=\"/blog/whats-in-the-box\" rel=\"noopener\">everything you get on day one</a>.</p>\n","date_published":"2026-08-18T00:00:00+00:00","date_modified":"2026-08-18T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["basics","getting started"]},{"id":"https://shiponeshot.com/blog/building-in-cursor","url":"https://shiponeshot.com/blog/building-in-cursor","title":"What is it like to build an app in Cursor?","summary":"Cursor puts the agent in your editor, with multi-file edits and a diff for everything. Here is how that changes the loop, and what to have set up first.","content_html":"<p>What is it like to build in Cursor when every change is a diff?</p>\n\n<p>It is the tightest of the agent loops, because you never leave the editor and you see every change\nin place before you accept it. The tradeoff is that the tight loop encourages small steps, which is\nexcellent for editing a feature and less suited to building a whole one in a pass. The setup that\nmakes it work is the same as everywhere else: a codebase worth reading, and something to run.</p>\n\n<h2 id=\"what-changes-when-the-agent-is-in-the-editor\">What changes when the agent is in the editor</h2>\n\n<p>Two things, and they pull in opposite directions.</p>\n\n<p><strong>The feedback is immediate.</strong> You see the change against the code around it, in the file you already\nhave open, with your own eyes on the surrounding context. This catches a whole class of \"technically\ncorrect, wrong for this file\" problems that are invisible in a terminal diff.</p>\n\n<p><strong>The unit of work gets smaller.</strong> Because accepting is one keystroke, the loop pulls toward many\nsmall edits rather than one complete change. That is great for refining and genuinely worse for\n\"build me this feature end to end\", where you want the model, the policy, the controller, the screens,\nand the tests to arrive as one coherent set.</p>\n\n<p>The practical answer is to use the wider, multi-file mode for the first pass of a feature and the\ninline loop for everything after.</p>\n\n<h2 id=\"have-these-before-you-start\">Have these before you start</h2>\n\n<p><strong>A guide file.</strong> Cursor will read the files you have open and the files it decides are relevant. A\nshort document stating the non-negotiable rules is the thing that keeps a small edit from quietly\nbreaking a convention that is enforced three directories away.</p>\n\n<p><strong>A worked example.</strong> The single most useful thing you can do is open the existing example of the\nthing you are building in another tab. Context that is already on screen is context it does not have\nto go looking for.</p>\n\n<p><strong>One command that verifies.</strong> In this codebase that is <code>bin/check</code>. Small edits accumulate, and the\nrisk of the tight loop is that twelve individually reasonable changes add up to something that does\nnot pass.</p>\n\n<h2 id=\"building-a-feature\">Building a feature</h2>\n\n<p>The first pass, in the multi-file mode:</p>\n<pre class=\"highlight\"><code>Add a Bookmarks resource following the same shape as the example slice:\nmodel, policy, controller, views, routes, and specs.\n</code></pre>\n<p>Naming the shape matters more than describing it. \"Following the same shape as the example slice\"\npoints at a file that already exists, which is far more precise than any description you could type.</p>\n\n<p>What comes back should touch the model, the migration, the permission policy, the controller, the\nscreens, the route, and the tests. Read the migration and the policy properly. Skim the rest.</p>\n\n<p>Then the tight loop takes over: adjust the empty state, fix the copy, tighten a validation, add the\none test you actually care about. This is where being in the editor pays.</p>\n\n<h2 id=\"the-two-review-habits-worth-building\">The two review habits worth building</h2>\n\n<p><strong>Look at what the diff touched, not just what it says.</strong> The change summary is a description of\nintent. The file list is a description of blast radius. When those disagree, believe the file list.</p>\n\n<p><strong>Run the checks before you accept a run of edits, not after ten of them.</strong> The tight loop makes it\neasy to get twelve edits deep before running anything, and then the failure could be from any of\nthem. Verifying often makes each failure cheap to locate.</p>\n\n<h2 id=\"where-it-goes-wrong\">Where it goes wrong</h2>\n\n<p><strong>Context you did not intend.</strong> Having the wrong file open can steer a change. If a suggestion is\noddly shaped, check what is in your tabs before blaming the model.</p>\n\n<p><strong>Accepting is frictionless, which is the point and the problem.</strong> One keystroke to accept means it\nis genuinely easy to accept something you did not read. The files where this matters are the\nmigration and the permission policy, always. Slow down for those two and move fast everywhere else.</p>\n\n<p><strong>Small steps hide structural drift.</strong> Twelve good local edits can arrive at a structure nobody would\nhave chosen deliberately. Every so often, step back and read the whole feature as one thing.</p>\n\n<h2 id=\"what-it-does-not-change\">What it does not change</h2>\n\n<p>The <a href=\"/blog/what-your-agent-should-not-build\" rel=\"noopener\">four categories where generated code is plausibly wrong</a>\nare no more visible in an editor diff than in a terminal one. Sign-in, payments, email\ndeliverability, and customer data separation are best solved by already having them, tested, before\nthe first prompt.</p>\n\n<p>The same loop with other agents: <a href=\"/blog/building-with-claude-code\" rel=\"noopener\">Claude Code</a>,\n<a href=\"/blog/building-with-codex\" rel=\"noopener\">Codex</a>, <a href=\"/blog/building-with-hermes\" rel=\"noopener\">Hermes</a>, and\n<a href=\"/blog/building-with-openclaw\" rel=\"noopener\">OpenClaw</a>.</p>\n","date_published":"2026-08-14T00:00:00+00:00","date_modified":"2026-08-14T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["agents","cursor","walkthrough"]},{"id":"https://shiponeshot.com/blog/building-with-openclaw","url":"https://shiponeshot.com/blog/building-with-openclaw","title":"What is it like to build with a self-hosted agent?","summary":"Your own agent, your own model, driven from the chat apps you already use. Here is what that changes about building, and the two things to get right first.","content_html":"<p>What is it like to build with a self-hosted agent like OpenClaw?</p>\n\n<p>Two things are genuinely different, and everything else is the same as any other agent. First, you\nchoose the model, which means the quality and the cost of a session are yours to tune rather than\nsomeone else's product decision. Second, you drive it from wherever you already talk, so the barrier\nto asking is a message rather than a terminal. Both of those are real advantages, and both of them\nhave a matching failure mode worth planning for.</p>\n\n<h2 id=\"the-advantage-you-own-the-loop\">The advantage: you own the loop</h2>\n\n<p>Self-hosting means the agent runs on your infrastructure with your model of choice and your\nextensions. Practically, that buys three things:</p>\n\n<ul>\n<li><strong>Model choice per task.</strong> A cheap fast model for mechanical edits, a stronger one for the change\nyou are nervous about.</li>\n<li><strong>Your code stays where you put it.</strong> For some teams this is the entire reason, and it is a\nlegitimate one.</li>\n<li><strong>Extensions you control.</strong> If your workflow needs a capability, you add it rather than waiting.</li>\n</ul>\n\n<h2 id=\"the-matching-risk-capability-varies\">The matching risk: capability varies</h2>\n\n<p>The honest version of \"you choose the model\" is \"you can choose badly\". A weaker model on a\nplumbing-heavy task produces exactly the <a href=\"/blog/what-your-agent-should-not-build\" rel=\"noopener\">plausible-looking wrongness that is hardest to\ncatch</a>: a webhook that does not verify signatures, a query\nmissing its account filter.</p>\n\n<p>There are two defenses, and you want both.</p>\n\n<p><strong>Do not put the risky work in scope.</strong> If sign-in, payments, email, and customer data separation\nalready exist and are tested, then no model choice you make can get them wrong, because they are not\nbeing written. This is the strongest available mitigation and it is structural rather than\nbehavioral.</p>\n\n<p><strong>Make verification non-optional.</strong> One command that runs the tests, the linter, a security scan, a\ndependency audit, and a secret scan, and a rule that nothing is done until it exits clean. A weaker\nmodel with a strict check is far safer than a stronger model with no check.</p>\n\n<h2 id=\"the-other-advantage-asking-from-anywhere\">The other advantage: asking from anywhere</h2>\n\n<p>Kicking off work from a message thread genuinely changes what gets done, because it changes what gets\nstarted. The ideas that would have waited for you to sit down often just happen.</p>\n<pre class=\"highlight\"><code>add a \"share\" button to bookmarks that copies a public link\n</code></pre>\n<p>That is a whole request, typed on a phone.</p>\n\n<h2 id=\"the-matching-risk-short-requests-are-ambiguous-requests\">The matching risk: short requests are ambiguous requests</h2>\n\n<p>The same convenience that gets more done produces the vaguest prompts you will ever write. The\nexample above does not say whether a shared link should work for signed-out visitors, whether it\nshould expire, or whether the owner can revoke it. Those are three product decisions, and the agent\nwill make all three for you.</p>\n\n<p>Two habits fix most of it:</p>\n\n<ul>\n<li><strong>Say what you do not want.</strong> It is shorter than specifying what you do, and it prevents the\nover-building that vague prompts reliably cause.</li>\n<li><strong>Ask for a plan before a change</strong> when the request is loose. A two-line summary of what it intends\nto do costs seconds and catches the misunderstanding before there is a diff.</li>\n</ul>\n\n<h2 id=\"what-the-codebase-has-to-provide\">What the codebase has to provide</h2>\n\n<p>Independent of which model you point at it, the agent needs the same three things:</p>\n\n<ol>\n<li><strong>A guide file</strong> stating the rules. With a self-hosted setup you may be switching models between\nsessions, which makes written conventions more important, not less: the file is the only thing\nthat is constant across them.</li>\n<li><strong>A worked example</strong> to copy, so structure is found rather than invented.</li>\n<li><strong>One verification command</strong>, treated as the definition of done.</li>\n</ol>\n\n<h2 id=\"where-it-goes-wrong\">Where it goes wrong</h2>\n\n<p><strong>Silent capability drops.</strong> Change the model and the character of the output changes without any\nannouncement. If a session feels off, check what it is running before debugging the prompt.</p>\n\n<p><strong>Extensions that bypass the checks.</strong> The flexibility that makes self-hosting good also makes it\npossible to build a workflow that skips verification. Whatever else your setup does, make the check\nstep the one thing that cannot be routed around.</p>\n\n<p><strong>No paper trail.</strong> Work started from a chat thread can land with no review. Decide where changes get\nreviewed, and keep that place, even when the request came from a message.</p>\n\n<h2 id=\"the-summary\">The summary</h2>\n\n<p>Self-hosting moves the quality and cost dials into your hands. That is worth having, and it means the\nstructural safeguards matter more than they do elsewhere: a base where the dangerous parts are\nalready built and tested, a written set of conventions, and a check that cannot be skipped.</p>\n\n<p>Get those three right and the model you pick becomes a tuning decision rather than a risk. That is\nmost of <a href=\"/blog/what-a-starter-kit-is\" rel=\"noopener\">what a starter kit is for</a>.</p>\n\n<p>The same loop with other agents: <a href=\"/blog/building-with-claude-code\" rel=\"noopener\">Claude Code</a>,\n<a href=\"/blog/building-with-codex\" rel=\"noopener\">Codex</a>, <a href=\"/blog/building-in-cursor\" rel=\"noopener\">Cursor</a>, and\n<a href=\"/blog/building-with-hermes\" rel=\"noopener\">Hermes</a>.</p>\n","date_published":"2026-08-13T00:00:00+00:00","date_modified":"2026-08-13T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["agents","openclaw","walkthrough"]},{"id":"https://shiponeshot.com/blog/building-with-codex","url":"https://shiponeshot.com/blog/building-with-codex","title":"What does building with Codex actually look like?","summary":"Codex proposes multi-file changes and waits for approval. Here is how that loop works on a codebase with clear conventions, and what to review first.","content_html":"<p>What does building with Codex look like when you review every diff?</p>\n\n<p>It looks like a proposal, not a fait accompli. You describe the change, Codex reads the codebase and\ncomes back with a multi-file diff, and nothing happens until you approve it. On a codebase with clear\nconventions the proposal usually lands close enough to accept with one or two adjustments. On a\ncodebase without them, you spend the review arguing about structure instead of substance.</p>\n\n<p>The propose-then-approve loop is the whole character of working this way, and it rewards preparation\ndifferently from an agent that just runs.</p>\n\n<h2 id=\"why-conventions-matter-more-here\">Why conventions matter more here</h2>\n\n<p>When an agent executes directly, a wrong structural choice shows up as a failing test. When an agent\nproposes, a wrong structural choice shows up as a diff you have to evaluate, and evaluating structure\nis much slower than evaluating behavior.</p>\n\n<p>So the highest-value thing you can do before the first prompt is make the right structure obvious:</p>\n\n<ul>\n<li><strong>A guide file</strong> (<code>AGENTS.md</code>) stating the rules that are not negotiable.</li>\n<li><strong>A worked example</strong> of the shape you want copied, sitting in the codebase already.</li>\n<li><strong>A single verification command</strong> so that after you approve, there is an unambiguous check.</li>\n</ul>\n\n<p>With those, the proposal arrives already shaped like your codebase, and your review is about the\nproduct decisions rather than the file layout.</p>\n\n<h2 id=\"a-real-request\">A real request</h2>\n<pre class=\"highlight\"><code>Add subscriptions to the billing flow for a new \"team\" tier.\nFollow the conventions in AGENTS.md.\n</code></pre>\n<p>The second line does more work than it looks like. It tells Codex that there is a document with\nopinions in it, which changes what it reads before proposing.</p>\n\n<h2 id=\"reading-the-proposal\">Reading the proposal</h2>\n\n<p>A diff spanning six files is easy to skim and easy to skim badly. There is an order that catches the\nmost problems for the least reading, and it is not top to bottom.</p>\n\n<p><strong>First, the migration.</strong> Schema changes are the most expensive to reverse once there is data.\nCheck the column types, the null constraints, the indexes, and above all the foreign key to the\naccount. If a table holding customer data has no account reference, stop there; nothing downstream\ncan be correct.</p>\n\n<p><strong>Second, the permission policy.</strong> This is where a subtle mistake is invisible and serious. The\nquestion to ask is not \"does this allow the right people\" but \"what does it do when it is unsure\".\nDeny by default, and every query filtered to the current account.</p>\n\n<p><strong>Third, the query in the controller.</strong> Look for lookups that go through the model directly instead\nof through the current account. <code>Thing.find(params[:id])</code> and <code>Current.account.things.find(params[:id])</code>\nlook almost identical and differ by whether a customer can read another customer's record by\nguessing a number.</p>\n\n<p><strong>Then everything else.</strong> Screens, copy, and formatting are cheap to change later and rarely\ndangerous.</p>\n\n<h2 id=\"what-codex-is-good-at-here\">What Codex is good at here</h2>\n\n<p><strong>Consistency across files.</strong> A change that touches a model, a policy, a controller, screens, and\ntests is exactly the kind of thing where a human forgets the fifth file. A proposal that arrives as a\ncomplete set is easier to reason about than five separate edits.</p>\n\n<p><strong>Matching existing style.</strong> It reads the surrounding code and writes in it. On a codebase that is\ninternally consistent this is close to free; on one that is not, it will faithfully reproduce whatever\nit happened to read first.</p>\n\n<p><strong>Not surprising you.</strong> The thing people actually like about this loop is that nothing runs until you\nsay so. That is worth real money on a production codebase.</p>\n\n<h2 id=\"where-it-goes-wrong\">Where it goes wrong</h2>\n\n<p><strong>Vague requests produce large diffs.</strong> \"Improve the billing flow\" is an invitation to touch fifteen\nfiles. Ask for one change at a time; the review cost of a proposal grows faster than its size.</p>\n\n<p><strong>It cannot tell a convention from an accident.</strong> If your codebase does something inconsistently in\ntwo places, the proposal picks one, and it may pick the one you were planning to remove. This is not\na failure of the tool, it is a failure of the codebase to have an opinion.</p>\n\n<p><strong>Approval is not verification.</strong> A diff that reads correctly can still fail. Run the checks after\napproving, every time. The propose-then-approve loop makes it tempting to treat your read as the\ncheck, and your read is not the check.</p>\n\n<h2 id=\"the-thing-to-keep-doing-after-the-first-week\">The thing to keep doing after the first week</h2>\n\n<p>Fix the example, not the copies.</p>\n\n<p>Every feature Codex proposes will resemble the nearest existing feature. That makes your worked\nexample the highest-leverage file in the repository: improve it once and every future proposal\nimproves. Let a flaw sit in it and you will review the same flaw ten times, approving it each time\nbecause it matches what is already there.</p>\n\n<h2 id=\"what-this-does-not-solve\">What this does not solve</h2>\n\n<p>The <a href=\"/blog/what-your-agent-should-not-build\" rel=\"noopener\">four things where generated code looks right and is\nnot</a> are exactly the four things a diff review is worst at\ncatching, because they are correct-looking by construction. If sign-in, payments, email\ndeliverability, and customer data separation already exist and are tested in your codebase, they are\nnot in the diff, and the review stays about your product.</p>\n\n<p>That is most of the argument in <a href=\"/blog/what-a-starter-kit-is\" rel=\"noopener\">what a starter kit is</a>.</p>\n\n<p>The same loop with other agents: <a href=\"/blog/building-with-claude-code\" rel=\"noopener\">Claude Code</a>,\n<a href=\"/blog/building-in-cursor\" rel=\"noopener\">Cursor</a>, <a href=\"/blog/building-with-hermes\" rel=\"noopener\">Hermes</a>, and\n<a href=\"/blog/building-with-openclaw\" rel=\"noopener\">OpenClaw</a>.</p>\n","date_published":"2026-08-12T00:00:00+00:00","date_modified":"2026-08-12T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["agents","codex","walkthrough"]},{"id":"https://shiponeshot.com/blog/keeping-customer-data-separate","url":"https://shiponeshot.com/blog/keeping-customer-data-separate","title":"How do you keep one customer from seeing another's data?","summary":"One missing filter is all it takes, and it has no symptoms in development. The structure that makes correct scoping the default, not something you remember.","content_html":"<p>How do you keep one customer from seeing another customer's data?</p>\n\n<p>Pick one boundary, put it on every record, and make every query go through it by default rather than\nby memory. The failure is always the same: one query somewhere that forgot to filter. It has no\nsymptoms in development, no failing test unless you wrote one specifically, and no error in\nproduction. Somebody just sees somebody else's data.</p>\n\n<p>The only reliable defense is structural. Here is the structure.</p>\n\n<h2 id=\"pick-the-boundary-first\">Pick the boundary first</h2>\n\n<p>Before any code, answer one question: <strong>what owns a piece of customer data?</strong></p>\n\n<p>Usually it is an account or workspace rather than a user, because eventually somebody wants to invite\na colleague, and if your data belongs to individual users you will be rewriting everything to fix it.</p>\n\n<p>Choosing account-as-boundary on day one costs nothing even if you never ship teams. Choosing\nuser-as-boundary and changing your mind costs a migration of every table you have.</p>\n\n<p>So: <strong>an account is the boundary.</strong> A user belongs to an account. Every piece of customer data\nbelongs to an account.</p>\n\n<h2 id=\"the-three-rules\">The three rules</h2>\n\n<p>Write these down where your team and your coding agent will read them, because they only work if they\nare followed everywhere.</p>\n\n<h3 id=\"1-every-model-has-an-account-reference\">1. Every model has an account reference</h3>\n\n<p>Not most. Every model holding customer data, with a non-null constraint and a foreign key at the\ndatabase level.</p>\n\n<p>The database constraint matters. It means a row without an owner cannot exist, so \"which account does\nthis belong to?\" always has an answer. A nullable column invites a code path that forgets to set it,\nand then you have orphan records that no scoped query will ever return and nobody will ever find.</p>\n\n<h3 id=\"2-every-query-goes-through-the-current-account\">2. Every query goes through the current account</h3>\n\n<p>This is the load-bearing rule. Compare:</p>\n<pre class=\"highlight language-ruby\"><code><span class=\"no\">Thing</span><span class=\"p\">.</span><span class=\"nf\">find</span><span class=\"p\">(</span><span class=\"n\">params</span><span class=\"p\">[</span><span class=\"ss\">:id</span><span class=\"p\">])</span>                  <span class=\"c1\"># wrong: any id, any account</span>\n<span class=\"no\">Current</span><span class=\"p\">.</span><span class=\"nf\">account</span><span class=\"p\">.</span><span class=\"nf\">things</span><span class=\"p\">.</span><span class=\"nf\">find</span><span class=\"p\">(</span><span class=\"n\">params</span><span class=\"p\">[</span><span class=\"ss\">:id</span><span class=\"p\">])</span> <span class=\"c1\"># right: only this account's ids</span>\n</code></pre>\n<p>Those two lines look nearly identical, and they differ by whether a customer can read another\ncustomer's record by changing a number in the address bar. In the second version an id belonging to\nsomeone else simply is not found, so it returns a 404, which is also the correct thing to tell them:\nconfirming that a record exists but is not theirs is itself a small leak.</p>\n\n<p>The reason to make it a rule rather than a habit is that habits fail once and rules fail visibly. If\nevery query in your codebase starts from <code>Current.account</code>, then one that does not stands out in\nreview.</p>\n\n<h3 id=\"3-permission-checks-deny-by-default\">3. Permission checks deny by default</h3>\n\n<p>Every action asks a policy object whether it is allowed, and the base policy says no. A new action\nwith no rule written is forbidden, not permitted.</p>\n\n<p>This matters because the common failure is forgetting to add a check, not writing a wrong one. Deny\nby default turns forgetting into a visible error rather than an open door.</p>\n\n<p>The default scope on those policies should filter to the current account too, so listing records is\nscoped by the same mechanism as fetching one.</p>\n\n<h2 id=\"the-test-that-has-to-exist\">The test that has to exist</h2>\n\n<p>Every resource you add ships with this test:</p>\n\n<blockquote>\n<p>A signed-in user requests a record belonging to a different account. The response is a 404.</p>\n</blockquote>\n\n<p>That is it. It is three lines and it is the difference between believing your scoping works and\nknowing it.</p>\n\n<p>Write it for the first resource, then copy it for every one after. The value is not in the individual\ntest, it is that the test is part of the shape of a resource, so an agent copying the pattern copies\nthe test too.</p>\n\n<h2 id=\"why-development-never-catches-this\">Why development never catches this</h2>\n\n<p>Worth being explicit, because it explains why careful people ship this bug.</p>\n\n<p>On your laptop you have one account. Everything you can see belongs to you. An unscoped query returns\nexactly the same result as a scoped one, on every screen, every time. The app is marginally faster\nwithout the filter. Nothing is red.</p>\n\n<p>The bug becomes reachable the moment you have two customers, and it becomes visible the moment two\ncustomers' ids collide in someone's address bar. Between those two moments, it is present and silent.</p>\n\n<p>This is why the <a href=\"/blog/what-your-agent-should-not-build\" rel=\"noopener\">category of failure</a> matters more than the\nindividual bug: you cannot find it by testing normally, because testing normally is what hides it.</p>\n\n<h2 id=\"the-places-it-gets-missed\">The places it gets missed</h2>\n\n<p>Even with the rules, a few spots get forgotten. Check these specifically:</p>\n\n<ul>\n<li><strong>Background jobs.</strong> A job runs with no request and no current account. If it loads records\ndirectly, nothing scopes it. Pass the account in and scope explicitly.</li>\n<li><strong>Admin and support tooling.</strong> Legitimately crosses accounts, which makes it exactly where a\nmistake is most damaging. Keep it separate and obvious rather than a flag on a normal path.</li>\n<li><strong>Exports and reports.</strong> Often written quickly, often assembling data from several tables, often\nthe one place someone wrote a raw query.</li>\n<li><strong>Anything with an id in the address.</strong> File downloads, share links, callbacks.</li>\n<li><strong>Nested lookups.</strong> Scoping the parent does not scope the child. If you find a comment by id\nwithout going through its post, the scoping stopped one level up.</li>\n</ul>\n\n<h2 id=\"what-this-buys-you-later\">What this buys you later</h2>\n\n<p>Two things, and they are the reason to do it on day one rather than when it hurts.</p>\n\n<p><strong>Teams become easy.</strong> If data already belongs to accounts and users already belong to accounts, then\ninviting a second person to an account is a small feature rather than a rewrite.</p>\n\n<p><strong>Deletion becomes possible.</strong> \"Delete everything belonging to this customer\" is a real obligation in\nseveral jurisdictions. If every record has an account reference, that is a query. If ownership is\nimplied by relationships that vary per table, it is a project, and one you cannot be confident you\nfinished.</p>\n\n<h2 id=\"the-summary\">The summary</h2>\n\n<ul>\n<li>One boundary. Make it the account.</li>\n<li>An account reference on every model, non-null, with a foreign key.</li>\n<li>Every query through the current account. A foreign id returns 404.</li>\n<li>Permissions that deny by default, with the default scope filtered to the account.</li>\n<li>A tenant-isolation test shipped with every resource.</li>\n<li>Check jobs, exports, and admin tooling specifically, because those are where it gets missed.</li>\n</ul>\n\n<p>Get the structure right and correct scoping becomes the path of least resistance, which is the only\nversion of this that survives contact with a deadline. It is also the first item on <a href=\"/blog/safe-to-launch-checklist\" rel=\"noopener\">the checklist\nthat makes an app safe to launch</a>.</p>\n","date_published":"2026-08-10T00:00:00+00:00","date_modified":"2026-08-10T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["security","how-to"]},{"id":"https://shiponeshot.com/blog/start-a-blog-and-get-found","url":"https://shiponeshot.com/blog/start-a-blog-and-get-found","title":"How do you start a blog on your app and actually get found?","summary":"Files beat a database, and the biggest mistake is blocking the crawlers that actually send you traffic. The whole setup, including what we built for this site.","content_html":"<p>How do you start a blog on your app and actually get found?</p>\n\n<p>Store posts as files rather than in a database, publish the machine-readable surfaces that search\nengines and AI assistants look for, and make sure you are not accidentally blocking the crawlers that\nsend you traffic. The last one is the most common own goal, and the one nobody notices, because\nnothing about it produces an error.</p>\n\n<p>This post is the blog you are reading. Here is how it is built.</p>\n\n<h2 id=\"files-not-a-database\">Files, not a database</h2>\n\n<p>The instinct is a posts table with an admin interface. Resist it.</p>\n\n<p>A database-backed blog needs authentication, permissions, an editor, image uploads, a draft workflow,\nand a preview mode. That is a real product, and it is not the one you are building.</p>\n\n<p>Files skip all of it. A post is a markdown file with a few lines of metadata at the top. The filename\nis the address. You already have a text editor, and your writing gets reviewed and versioned exactly\nlike your code.</p>\n\n<p>The tradeoff is honest: publishing requires a deploy, and non-technical colleagues cannot post\nwithout one. For a product blog written by the people who build the product, that is a fine trade,\nand it is the right default until it stops being.</p>\n\n<h2 id=\"what-every-post-needs-generated-for-it\">What every post needs generated for it</h2>\n\n<p>This is the part worth automating once, because doing it by hand per post is how it gets skipped:</p>\n\n<ul>\n<li><strong>A canonical address.</strong> Build it from the path, never from the full URL including query strings.\nOtherwise a link with a campaign parameter becomes a second copy of the page and splits its\nranking.</li>\n<li><strong>Social card tags</strong>, so a shared link unfurls properly.</li>\n<li><strong>Structured data</strong>, marking the page as an article with an author, a publication date, and a\nmodification date.</li>\n<li><strong>A sitemap entry</strong> with a last-modified date.</li>\n<li><strong>A feed.</strong> Atom, and a JSON feed alongside it.</li>\n</ul>\n\n<p>None of this is per-post work if you generate it from the post's own metadata.</p>\n\n<h2 id=\"the-three-surfaces-most-sites-are-missing\">The three surfaces most sites are missing</h2>\n\n<h3 id=\"robots-txt-served-by-the-app\">robots.txt, served by the app</h3>\n\n<p>Usually a static file with a stale sitemap line. Serve it from the application instead and it can\nknow its own address, stay correct across environments, and survive a rebrand.</p>\n\n<h3 id=\"llms-txt\">llms.txt</h3>\n\n<p>A convention that is quickly becoming worth having: a markdown index of your site at <code>/llms.txt</code>,\nlisting your pages and posts with a one-line description each, so an assistant can understand what\nyou have in one fetch rather than by crawling. Pair it with a full-text version that inlines\neverything.</p>\n\n<h3 id=\"markdown-alternates\">Markdown alternates</h3>\n\n<p>Serve the raw source of each post at the same address with a <code>.md</code> extension, and link it from the\npage. A crawler or a tool that wants your text does not have to parse your HTML to get it.</p>\n\n<h2 id=\"the-mistake-that-quietly-costs-the-most\">The mistake that quietly costs the most</h2>\n\n<p>AI crawlers are not one thing. They come in three kinds, and they behave completely differently:</p>\n\n<table><thead>\n<tr>\n<th>Kind</th>\n<th>What it does</th>\n<th>Sends you traffic?</th>\n</tr>\n</thead><tbody>\n<tr>\n<td><strong>Training</strong></td>\n<td>Builds model weights</td>\n<td>No</td>\n</tr>\n<tr>\n<td><strong>Search index</strong></td>\n<td>Builds the assistant's retrieval index</td>\n<td>Yes, as citations</td>\n</tr>\n<tr>\n<td><strong>Live fetch</strong></td>\n<td>Fetches on demand when someone asks</td>\n<td>Yes, directly</td>\n</tr>\n</tbody></table>\n\n<p>Most sites that decide to \"block AI\" write rules aimed at the training crawlers and take out the\nfetchers by accident. The result is that they disappear from AI answers entirely, while every other\nmetric looks completely normal.</p>\n\n<p>If you want traffic from assistants, the search-index and live-fetch categories are the ones to\nwelcome, explicitly, by name.</p>\n\n<p>Two things people get wrong here. <code>Google-Extended</code> and <code>Applebot-Extended</code> are <strong>not crawlers</strong>;\nthey are training opt-out tokens, and disallowing them does not remove you from AI Overviews. And a\ncrawler cannot be blocked by robots.txt alone if something in front of your app is already refusing\nit.</p>\n\n<h2 id=\"three-layers-that-can-make-you-invisible-silently\">Three layers that can make you invisible, silently</h2>\n\n<p>This is the part worth checking today, whatever else you take from this post. Any of these can hide\nyour site with no error, no log line, and no symptom:</p>\n\n<ol>\n<li><strong>Your CDN.</strong> Providers now ship a \"block AI crawlers\" control that rejects requests at the edge.\nA perfect robots.txt changes nothing if that is on.</li>\n<li><strong>Browser version checks.</strong> Frameworks that turn away outdated browsers answer with a bare refusal.\nExclude your public content from that check; an article is prose, not an application.</li>\n<li><strong>Rate limiting.</strong> A blanket per-address limit will catch a crawler sweeping your site, and a\nrefusal is read as \"this site is unhealthy, come back less often\".</li>\n</ol>\n\n<p>One command tests all three at once:</p>\n<pre class=\"highlight language-bash\"><code>curl <span class=\"nt\">-sI</span> <span class=\"nt\">-A</span> <span class=\"s2\">\"OAI-SearchBot\"</span> https://yourdomain.com/blog | <span class=\"nb\">head</span> <span class=\"nt\">-1</span>\n</code></pre>\n<p>If that is not a 200, nothing else on this page matters.</p>\n\n<h2 id=\"write-so-you-can-be-quoted\">Write so you can be quoted</h2>\n\n<p>The structural work above gets you readable. This gets you cited.</p>\n\n<p>An assistant lifts a passage from your page and shows it with no surrounding context. So:</p>\n\n<ol>\n<li><strong>Make the title the question someone types.</strong> Not a clever phrase.</li>\n<li><strong>Restate the question in the first sentence.</strong></li>\n<li><strong>Answer it within eighty words</strong>, with something concrete: a number, a list, a yes with its\ncondition. Assume this paragraph will be shown alone, because it will be.</li>\n<li><strong>Then go deep</strong>, under headings that are themselves nearly questions. Each section must make\nsense read on its own.</li>\n<li><strong>Prefer specifics to adjectives.</strong></li>\n<li><strong>State your limits honestly.</strong> Sources that qualify their claims get cited more, not less.</li>\n</ol>\n\n<p>The last piece of advice is the one that matters most: write about the thing where you are genuinely\nthe best available source. Anyone can restate a popular tutorial, and the results already have forty\nof those. Nobody else can explain what your product actually does, what it costs, or what you learned\nbuilding it.</p>\n\n<p>That is the post worth writing first.</p>\n","date_published":"2026-08-08T00:00:00+00:00","date_modified":"2026-08-08T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["writing","seo","how-to"]},{"id":"https://shiponeshot.com/blog/building-with-claude-code","url":"https://shiponeshot.com/blog/building-with-claude-code","title":"What does building with Claude Code actually look like?","summary":"A walkthrough of the real loop: what you type, what it reads, what it writes, and where you step in. One prompt and one review for a tested feature.","content_html":"<p>What does building a paid feature with Claude Code actually look like?</p>\n\n<p>It looks like one clear instruction, a few minutes of the agent reading before it writes anything, a\ndiff covering six or seven files, a test run it does itself, and one review pass from you. The\nfeature that comes back is account-scoped, permission-checked, paywalled, and tested, because those\nare the conventions it found in the codebase rather than things you asked for.</p>\n\n<p>Here is the loop in detail, and the parts where it goes wrong.</p>\n\n<h2 id=\"the-setup-that-makes-the-difference\">The setup that makes the difference</h2>\n\n<p>Claude Code works in your terminal. It reads files, writes changes, and runs commands, which means it\ncan check its own work. That last capability is the one that changes the character of a session, and\nit only pays off if there is something worth running.</p>\n\n<p>So before anything else, the codebase needs two things:</p>\n\n<ol>\n<li><strong>A guide file it reads first.</strong> <code>CLAUDE.md</code> in the project root, telling it the one right way to\ndo each thing here.</li>\n<li><strong>A test suite it can run.</strong> <code>bin/check</code> in this case, which runs the tests, the linter, a static\nsecurity scan, a dependency audit, and a secret scan in one command.</li>\n</ol>\n\n<p>Without the first, it guesses at your conventions. Without the second, \"done\" is something it asserts\nrather than something it verifies.</p>\n\n<h2 id=\"the-instruction\">The instruction</h2>\n\n<p>Here is the entire prompt for a real feature:</p>\n<pre class=\"highlight\"><code>Add a paid Bookmarks feature. A bookmark has a url, a title, and an optional note.\nUse the crud-with-billing skill.\n</code></pre>\n<p>That is it. Note what is not in there: no mention of accounts, no mention of permissions, no mention\nof tests, no mention of the design system. All of that is already written down, and repeating it in\nthe prompt is how you end up with a prompt that is longer than the feature.</p>\n\n<h2 id=\"what-it-does-before-writing-anything\">What it does before writing anything</h2>\n\n<p>The first thing that happens is reading, and it is worth watching rather than skipping.</p>\n\n<p>It opens <code>CLAUDE.md</code> and finds the rule that governs everything: every model belongs to an account,\nevery query goes through the current account, every controller checks permissions. Then it opens the\nworked example that already exists in the codebase, the one deliberately left there to be copied, and\nreads the whole vertical slice: the model, the permission policy, the controller, the screens, and\nthe tests.</p>\n\n<p>By the time it writes its first line it has a template. It is not inventing a structure, it is\nfollowing one.</p>\n\n<h2 id=\"the-diff\">The diff</h2>\n\n<p>What comes back is about seven files:</p>\n\n<ul>\n<li>The model and its migration, with the account reference and validations.</li>\n<li>A permission policy that denies by default and filters every query to the current account.</li>\n<li>A controller that queries through the current account rather than through the model directly, and\ncarries the one line that requires a paid subscription.</li>\n<li>Screens, using the existing design system classes rather than inventing new ones.</li>\n<li>The route.</li>\n<li>Model, permission, and request tests, including the one that matters most: a request for another\naccount's bookmark returns a 404 rather than someone else's data.</li>\n</ul>\n\n<p>That last test is the one people forget when they write this by hand, and it is the difference\nbetween a feature and an incident.</p>\n\n<h2 id=\"where-you-actually-step-in\">Where you actually step in</h2>\n\n<p>Three places, and none of them is the code style.</p>\n\n<p><strong>The data model.</strong> The agent will make reasonable choices and some of them will be wrong for your\nproduct. Should a bookmark belong to a user or to the whole account? Should the url be unique per\naccount? These are product decisions wearing schema clothing, and they are much cheaper to fix now\nthan after there is data.</p>\n\n<p><strong>Whether it should be paid at all.</strong> You said paid, so it made it paid. Whether that is right is not\nsomething it can know.</p>\n\n<p><strong>The empty state and the copy.</strong> It will write something sensible and generic. Sensible and generic\nis exactly what your product should not sound like.</p>\n\n<h2 id=\"where-it-goes-wrong\">Where it goes wrong</h2>\n\n<p>Being honest about the failure modes, because they are consistent.</p>\n\n<p><strong>It over-builds when the request is vague.</strong> \"Add bookmarks\" with no further detail produces\nbookmarks with tags, folders, search, and bulk import. Every one of those is a thing you now own.\nBeing specific about what you do <em>not</em> want is worth more than being specific about what you do.</p>\n\n<p><strong>It follows a bad example faithfully.</strong> The flip side of pattern-matching your codebase is that it\nwill copy your mistakes with the same enthusiasm as your conventions. If the worked example has a\nflaw, every feature built after it inherits the flaw. Fix the example, not the copies.</p>\n\n<p><strong>It declares victory on a partial run.</strong> If the test command is slow or noisy it will sometimes\nsummarize rather than actually check. The fix is a single command that exits non-zero on any failure,\nand a guide file that names it as the definition of done.</p>\n\n<h2 id=\"the-part-that-surprises-people\">The part that surprises people</h2>\n\n<p>The tests. Not that the agent writes them, but that it uses them.</p>\n\n<p>A run where the suite goes red partway through does not come back to you broken. It reads the\nfailure, fixes it, and runs again. You see the finished state. This is the practical difference\nbetween an agent that can execute commands and one that only writes text, and it is why the setup\nwork at the top of this post is worth doing before the first feature rather than after the third.</p>\n\n<h2 id=\"what-this-does-not-remove\">What this does not remove</h2>\n\n<p>You still have to review. The diff is small and it is entirely about your feature, which makes the\nreview genuinely quick, but \"quick\" is not \"unnecessary\". Read the migration and the permission\npolicy properly, every time. Those are the two files where a mistake is expensive and silent.</p>\n\n<p>And it does not remove the need to know what you are building. The agent is very fast at producing\nthe thing you described. It has no opinion about whether the thing was worth describing.</p>\n\n<h2 id=\"if-you-want-to-try-it\">If you want to try it</h2>\n\n<p>The shape above works because the codebase was arranged for it: a guide file, a worked example, one\nverification command, and the plumbing already built and tested. That is <a href=\"/blog/what-a-starter-kit-is\" rel=\"noopener\">what a starter kit\nis</a>, and it is <a href=\"/blog/where-the-first-run-goes\" rel=\"noopener\">why the first run otherwise disappears into\nplumbing</a>.</p>\n\n<p>The same loop works with <a href=\"/blog/building-with-codex\" rel=\"noopener\">Codex</a>, <a href=\"/blog/building-in-cursor\" rel=\"noopener\">Cursor</a>,\n<a href=\"/blog/building-with-hermes\" rel=\"noopener\">Hermes</a>, and <a href=\"/blog/building-with-openclaw\" rel=\"noopener\">OpenClaw</a>, with differences\nworth knowing about in each.</p>\n","date_published":"2026-08-07T00:00:00+00:00","date_modified":"2026-08-07T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["agents","claude code","walkthrough"]},{"id":"https://shiponeshot.com/blog/email-that-arrives","url":"https://shiponeshot.com/blog/email-that-arrives","title":"Why does your app's email go to spam, and how do you fix it?","summary":"Almost always missing DNS records, not your code. What SPF, DKIM, and DMARC do, the order to set them up, and the From-address mistake that undoes all three.","content_html":"<p>Why does your app's email go to spam, and how do you fix it?</p>\n\n<p>Because you are missing DNS records, not because your code is wrong. Three records decide whether\nyour mail is trusted: <strong>SPF</strong> says which servers may send for your domain, <strong>DKIM</strong> signs each\nmessage cryptographically, and <strong>DMARC</strong> tells receiving servers what to do when the first two fail.\nPublish all three, make sure your From address is on the domain you signed with, and the problem\nusually disappears within a day or two.</p>\n\n<p>The code is two lines. This is the other 95% of the work.</p>\n\n<h2 id=\"why-this-matters-more-than-it-used-to\">Why this matters more than it used to</h2>\n\n<p>Two things changed.</p>\n\n<p>Large mailbox providers tightened their requirements, and messages from domains with no\nauthentication now get treated with more suspicion than they used to. And if your product uses\n<a href=\"/blog/sign-in-without-passwords\" rel=\"noopener\">passwordless sign-in</a>, your email is not marketing, it is your\nfront door. Mail in spam means nobody can log in, and there is no error anywhere to tell you.</p>\n\n<h2 id=\"the-three-records\">The three records</h2>\n\n<h3 id=\"spf-which-servers-may-send\">SPF: which servers may send</h3>\n\n<p>A DNS record listing the servers allowed to send mail for your domain. A receiver checks the sending\nserver against that list.</p>\n\n<p>Your email provider tells you what to publish. It usually looks like:</p>\n<pre class=\"highlight\"><code>v=spf1 include:_spf.yourprovider.com ~all\n</code></pre>\n<p>Two things people get wrong:</p>\n\n<ul>\n<li><strong>Only one SPF record per domain.</strong> Two records is not \"extra coverage\", it is a syntax error, and\nit fails both. If you already have one, merge the new <code>include:</code> into it.</li>\n<li><strong>There is a lookup limit.</strong> SPF permits ten DNS lookups, and each <code>include:</code> counts. Stack up\nenough providers and it silently starts failing.</li>\n</ul>\n\n<h3 id=\"dkim-a-signature-on-every-message\">DKIM: a signature on every message</h3>\n\n<p>Your provider signs outgoing mail with a private key. You publish the matching public key in DNS.\nThe receiver verifies the signature and knows the message was not altered and really came from you.</p>\n\n<p>Your provider generates this and gives you the record to publish. There is not much to get wrong\nbeyond pasting it correctly, and it is the strongest of the three signals.</p>\n\n<h3 id=\"dmarc-what-to-do-when-the-first-two-fail\">DMARC: what to do when the first two fail</h3>\n\n<p>This is the one people skip, because mail appears to work without it, and it is increasingly the one\nthat decides whether you are trusted.</p>\n\n<p>DMARC does two jobs: it tells receivers your policy for failures, and it makes them check\n<strong>alignment</strong>, which is the part that actually catches spoofing.</p>\n\n<p>Start here, on day one:</p>\n<pre class=\"highlight\"><code>v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com\n</code></pre>\n<p><code>p=none</code> means \"do not change anything, just send me reports\". It is safe to publish immediately and\nit starts giving you data about who is sending as you.</p>\n\n<p>Once the reports look clean, tighten it:</p>\n<pre class=\"highlight\"><code>v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com\n</code></pre>\n<p>Then eventually <code>p=reject</code>. Do not start at reject. If something legitimate is misconfigured, you\nwill stop your own mail, and the first thing you will notice is customers unable to sign in.</p>\n\n<h2 id=\"the-mistake-that-undoes-all-three\">The mistake that undoes all three</h2>\n\n<p>Here is the one that catches people who did everything else right.</p>\n\n<p>You verify <code>yourdomain.com</code> with your email provider. DKIM signs as <code>yourdomain.com</code>. Then you set\nyour From address to <code>hello@somethingelse.com</code>, perhaps because that is where you read replies.</p>\n\n<p>Now DKIM signs one domain while the From header claims another. <strong>Alignment fails</strong>, DMARC fails, and\nyour carefully configured setup is worse than useless, because you are now producing exactly the\nsignature-mismatch pattern that spoofing filters exist to catch.</p>\n\n<p>The rule: <strong>the From address must be on the domain you authenticated.</strong></p>\n\n<p>If you want replies somewhere else, forward the address at your provider. Do not solve it with a\nReply-To on a different domain, and do not solve it by changing the From.</p>\n\n<h2 id=\"the-order-to-do-it-in\">The order to do it in</h2>\n\n<ol>\n<li><strong>Pick a provider and verify your domain.</strong> They will give you the SPF and DKIM records.</li>\n<li><strong>Publish SPF and DKIM.</strong> Wait for propagation, which is usually minutes but can be longer.</li>\n<li><strong>Publish DMARC at <code>p=none</code></strong> with a reporting address. Do this on day one; it costs nothing and\nstarts collecting evidence.</li>\n<li><strong>Send a test message and read the headers.</strong> Check that SPF, DKIM, and DMARC all show as passing.\nDo not skip this and assume; the whole point is that failure is silent.</li>\n<li><strong>Watch the reports for a couple of weeks.</strong></li>\n<li><strong>Tighten to <code>p=quarantine</code></strong>, then later to <code>p=reject</code>.</li>\n</ol>\n\n<h2 id=\"the-code-part-briefly\">The code part, briefly</h2>\n\n<p>Almost trivial by comparison, but three things are worth getting right.</p>\n\n<p><strong>Send both HTML and plain text.</strong> A message with no plain text alternative looks worse to filters,\nand some clients genuinely prefer it.</p>\n\n<p><strong>Send outside the web request.</strong> A slow mail provider should never slow down a page. Queue the\nmessage and deliver it in a <a href=\"/blog/background-work-without-extra-services\" rel=\"noopener\">background job</a>. This is\nalso what makes retries possible when the provider has a bad minute.</p>\n\n<p><strong>Do not send real mail in development.</strong> Every developer has sent a test message to a real customer.\nUse a local preview that opens the message in a browser instead of delivering it, so nothing can\nescape from a laptop.</p>\n\n<h2 id=\"transactional-and-marketing-mail-should-be-separate\">Transactional and marketing mail should be separate</h2>\n\n<p>Worth knowing before it hurts.</p>\n\n<p>Your sign-in codes and receipts are transactional. Your newsletter is marketing. If they share a\nsending domain and enough people mark the newsletter as spam, the reputation damage takes your\nsign-in codes down with it.</p>\n\n<p>The usual fix is a subdomain for marketing, keeping the reputation of your transactional mail\nseparate from anything anyone might complain about.</p>\n\n<h2 id=\"the-checklist\">The checklist</h2>\n\n<ul>\n<li>One SPF record, within the lookup limit.</li>\n<li>DKIM published and verified.</li>\n<li>DMARC published, starting at <code>p=none</code>, tightened once the reports are clean.</li>\n<li><strong>From address on the domain you signed with.</strong> No exceptions.</li>\n<li>HTML and plain text in every message.</li>\n<li>Delivery in a background job.</li>\n<li>Local preview in development, so nothing real is ever sent by accident.</li>\n<li>Marketing mail on a separate subdomain.</li>\n</ul>\n\n<p>This is one of the <a href=\"/blog/what-your-agent-should-not-build\" rel=\"noopener\">four things worth not building\nyourself</a>, and it is the clearest case of all four: no amount\nof better code fixes it, because the code was never the problem.</p>\n","date_published":"2026-08-05T00:00:00+00:00","date_modified":"2026-08-05T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["email","how-to"]},{"id":"https://shiponeshot.com/blog/your-app-in-the-app-stores","url":"https://shiponeshot.com/blog/your-app-in-the-app-stores","title":"How do you get your app into the App Store and Play Store?","summary":"Wrap it in a native shell that loads your existing screens. What it costs, what the stores require, and the in-app purchase rule that decides your economics.","content_html":"<p>How do you get your web app into the App Store and Play Store?</p>\n\n<p>Wrap it in a thin native shell that loads your existing web screens, add native navigation so it does\nnot feel like a browser, and wire up in-app purchases because both stores require them for digital\ngoods. You maintain one codebase and ship to three places. The costs are <strong>$99 per year</strong> for Apple,\n<strong>$25 once</strong> for Google, and <strong>up to 30%</strong> of every purchase made inside the app, which is usually\nthe number that decides whether this is worth doing at all.</p>\n\n<h2 id=\"the-approach\">The approach</h2>\n\n<p>You do not rebuild your product twice. You build a small native application whose main job is to\ndisplay your web screens, plus the parts that genuinely have to be native.</p>\n\n<p>That is not the same as shipping a bare browser window. A shell like this gives you:</p>\n\n<ul>\n<li><strong>Native navigation.</strong> Screens push and pop with the platform's own transitions and gestures, which\nis most of what makes an app feel like an app.</li>\n<li><strong>Native controls where they matter.</strong> Tab bars, the system share sheet, the camera, notifications.</li>\n<li><strong>Rules about which screens behave how</strong>, served from your server, so you can change navigation\nbehavior without shipping a new build through review.</li>\n</ul>\n\n<p>That last point is worth dwelling on. Anything driven by your server changes when you deploy.\nAnything compiled into the app waits for review. Keeping as much as possible on the server side is\nthe difference between a two-hour fix and a two-day one.</p>\n\n<h2 id=\"what-has-to-be-native\">What has to be native</h2>\n\n<p>Be realistic about the boundary:</p>\n\n<ul>\n<li><strong>In-app purchases.</strong> Required. Not optional. More below.</li>\n<li><strong>Push notifications.</strong></li>\n<li><strong>Anything using hardware</strong>: camera, location, biometrics.</li>\n<li><strong>The launch screen and icon.</strong></li>\n</ul>\n\n<p>Everything else can be a web screen, and should be, because it is one implementation instead of\nthree.</p>\n\n<h2 id=\"the-in-app-purchase-rule-which-decides-your-economics\">The in-app purchase rule, which decides your economics</h2>\n\n<p>If you sell digital goods or subscriptions that are used inside your app, both stores require you to\nsell them through their purchase system, and both take a cut of up to 30%, reduced for smaller\ndevelopers under their respective programs.</p>\n\n<p>You cannot avoid this by linking out to your own checkout from inside the app. That gets rejected,\nand the rules around what you may and may not say about outside payment options have shifted\nrepeatedly. Read the current guidelines rather than a blog post, including this one.</p>\n\n<p>The strategic consequence: <strong>your web checkout and your in-app checkout are different businesses</strong>\nwith different margins. Many products deliberately steer signups to the web, where they keep the full\namount, and treat in-app purchase as a convenience for people who found them in the store.</p>\n\n<h2 id=\"the-part-that-gets-architecturally-messy\">The part that gets architecturally messy</h2>\n\n<p>You now have two ways for someone to pay, and they arrive as completely different things: a payment\nprovider's webhook on one side, and a store receipt on the other.</p>\n\n<p>The trap is building two parallel systems that each track what a customer has paid for, and then\ndiscovering they disagree. Somebody subscribes on the web and opens the app, which does not know. Or\nsubscribes in the app and uses the web, which does not know.</p>\n\n<p>The fix is to make <strong>entitlement a single concept</strong>. One record per account answering \"is this\naccount currently allowed to use the paid feature, and until when\". Both payment paths write to it.\nEverything else reads it and does not care where the money came from.</p>\n\n<p>Get that right and adding a third payment route later is a small job. Get it wrong and every new\nroute multiplies the states you have to reconcile.</p>\n\n<h2 id=\"what-review-actually-catches\">What review actually catches</h2>\n\n<p>Both stores review submissions. The rejections that come up repeatedly:</p>\n\n<ul>\n<li><strong>Looking too much like a website.</strong> If it is visibly a browser window with your site in it, expect\na rejection. Native navigation is the main thing that fixes this.</li>\n<li><strong>Payment rules.</strong> Any route to paying that is not their route.</li>\n<li><strong>Sign in with Apple.</strong> If you offer other social sign-in options on iOS, you are generally required\nto offer Apple's too.</li>\n<li><strong>Account deletion.</strong> If someone can create an account in the app, they must be able to delete it\nfrom the app. Not by emailing support.</li>\n<li><strong>Broken or unreachable screens.</strong> Reviewers do click things.</li>\n<li><strong>Privacy disclosures</strong> that do not match what the app actually collects.</li>\n</ul>\n\n<p>Most of these are cheap if you know them in advance and expensive if you find them at submission,\nbecause each round trip through review costs days.</p>\n\n<h2 id=\"the-realistic-timeline\">The realistic timeline</h2>\n\n<p>For a product whose web app already exists and works:</p>\n\n<table><thead>\n<tr>\n<th>Work</th>\n<th>Time</th>\n</tr>\n</thead><tbody>\n<tr>\n<td>Shell running, loading your screens</td>\n<td>1 to 2 days</td>\n</tr>\n<tr>\n<td>Native navigation rules</td>\n<td>1 to 3 days</td>\n</tr>\n<tr>\n<td>In-app purchase, both platforms</td>\n<td>3 to 7 days</td>\n</tr>\n<tr>\n<td>Icons, launch screens, store listings, screenshots</td>\n<td>2 to 3 days</td>\n</tr>\n<tr>\n<td>Review, first submission</td>\n<td>1 to 7 days, plus rework</td>\n</tr>\n</tbody></table>\n\n<p>Call it two to three weeks including the first rejection, which you should assume rather than hope\nagainst.</p>\n\n<p>The in-app purchase line is the one that surprises people. It is not just a payment button, it is\nreceipt validation, restoring purchases, renewals, and the notifications that tell your server when a\nsubscription changed state. It is the same <a href=\"/blog/charging-for-your-app\" rel=\"noopener\">source-of-truth problem as web\npayments</a>, in a different shape.</p>\n\n<h2 id=\"is-it-worth-it\">Is it worth it?</h2>\n\n<p>The honest answer is that it depends on where your customers look for you.</p>\n\n<p><strong>Probably yes</strong> if your product is used on a phone regularly, if being findable in a store matters\nto your audience, or if you need notifications or hardware access.</p>\n\n<p><strong>Probably not yet</strong> if your product is used at a desk, if your customers find you through search or\nword of mouth, or if you have not validated the web version. Two to three weeks plus 30% of in-app\nrevenue is a real price for a channel that may not be where your customers are.</p>\n\n<p>If you do go ahead, get the entitlement model right first. That single decision determines whether\nthe second payment route is a week of work or a permanent source of support tickets.</p>\n","date_published":"2026-08-04T00:00:00+00:00","date_modified":"2026-08-04T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["mobile","how-to"]},{"id":"https://shiponeshot.com/blog/agent-guide-file","url":"https://shiponeshot.com/blog/agent-guide-file","title":"How do you write a guide file your agent will follow?","summary":"Short, specific, and about decisions rather than description. What belongs in a guide file, what to leave out, and why most of them get ignored.","content_html":"<p>How do you write a guide file your coding agent will actually follow?</p>\n\n<p>Keep it under about 150 lines, make every line a decision rather than a description, and point at\nreal files instead of explaining them. The failure mode is not that agents ignore instructions, it is\nthat most guide files are documentation, and documentation tells you what exists rather than what to\ndo. A guide file is a map of choices: here is the one right way to do this thing, here is the file to\ncopy, here is the rule that must never be broken.</p>\n\n<h2 id=\"why-most-of-them-get-ignored\">Why most of them get ignored</h2>\n\n<p>Open a typical <code>CLAUDE.md</code> or <code>AGENTS.md</code> and it reads like a README: an overview of the project, a\nlist of directories, a description of the stack, some setup commands.</p>\n\n<p>None of that changes a single decision. The agent can already see the directory structure. It can\nread the dependency file. Telling it \"we use a background job system\" is not information; telling it\n\"background work goes through the existing job class, never a thread, and here is the one to copy\"\nis.</p>\n\n<p>The test for every line: <strong>if the agent had not read this, would it have done something different?</strong>\nIf not, cut the line. A 600-line guide file where 40 lines matter performs worse than a 40-line one,\nbecause the signal is diluted and the important rules are somewhere in the middle.</p>\n\n<h2 id=\"what-actually-belongs-in-it\">What actually belongs in it</h2>\n\n<h3 id=\"the-rule-that-must-never-be-broken\">The rule that must never be broken</h3>\n\n<p>Every codebase has one or two invariants where a violation is serious and silent. Put them first, in\nplain language, with the consequence stated.</p>\n\n<p>In this codebase it is data separation:</p>\n\n<blockquote>\n<p>Every model gets an account reference. Every query is scoped to the current account. Every\ncontroller checks permissions. Break this rule and one customer sees another's data.</p>\n</blockquote>\n\n<p>That is short, it is unambiguous, and it names the cost. An agent reading it has no room to decide\nthat this particular query is an exception.</p>\n\n<h3 id=\"the-one-obvious-way-to-do-each-common-thing\">The one obvious way to do each common thing</h3>\n\n<p>Not the options. The choice.</p>\n\n<p>An agent that finds three plausible ways to add a feature will pick one, and it will not be the same\none next time. Consistency across features is worth more than any individual choice being optimal, so\nmake the choice once and write it down.</p>\n\n<h3 id=\"pointers-to-real-files\">Pointers to real files</h3>\n\n<p>This is the highest-leverage thing in the whole document, and it is why a guide file can be short.</p>\n\n<p>Instead of describing the shape of a feature over two pages, name the file that already has that\nshape:</p>\n\n<blockquote>\n<p>The canonical example is the example slice: read <code>app/models/project.rb</code>,\n<code>app/policies/project_policy.rb</code>, <code>app/controllers/projects_controller.rb</code>, and its specs, then\ncopy that shape.</p>\n</blockquote>\n\n<p>Code is more precise than prose about code, it does not drift out of date, and the agent is better at\nreading it than at reading your description of it.</p>\n\n<h3 id=\"the-definition-of-done\">The definition of done</h3>\n\n<p>One command, and the statement that it must pass.</p>\n\n<blockquote>\n<p>Verify your work: <code>bin/check</code>. This is the definition of done. It must be green before you are\nfinished.</p>\n</blockquote>\n\n<p>Without this an agent decides for itself when it has finished, and the only evidence available to it\nis that the code it wrote looks like code that works.</p>\n\n<h3 id=\"the-traps-that-fail-silently\">The traps that fail silently</h3>\n\n<p>The most valuable and most commonly missing section. Every mature codebase has a few landmines where\nthe failure produces no error at all: a policy that blocks something with no message, a check that\nreturns an empty result rather than raising, a configuration that has to be updated in two places.</p>\n\n<p>An agent cannot discover these by reading, because there is nothing to read. Write them down, with\nthe symptom:</p>\n\n<blockquote>\n<p>The content security policy is enforced and it fails silently. Sending a user to an external host\nmeans adding that host to the policy. Miss it and the button does nothing: no exception, no\nmessage, nothing in the network tab.</p>\n</blockquote>\n\n<p>That paragraph saves an hour of confused debugging every time it is read.</p>\n\n<h2 id=\"what-to-leave-out\">What to leave out</h2>\n\n<ul>\n<li><strong>Anything the agent can see.</strong> Directory listings, dependency lists, the framework version.</li>\n<li><strong>General best practice.</strong> \"Write tests\", \"use meaningful names\", \"handle errors\". It knows.\nRepeating it costs attention and changes nothing.</li>\n<li><strong>History.</strong> Why you migrated away from a thing three years ago is interesting and irrelevant.</li>\n<li><strong>Aspirations.</strong> Rules you do not actually follow are worse than no rules, because they teach the\nagent that this document describes an imaginary codebase.</li>\n</ul>\n\n<h2 id=\"write-it-for-the-failure-modes-you-have-seen\">Write it for the failure modes you have seen</h2>\n\n<p>The best guide files are grown, not drafted.</p>\n\n<p>Every time an agent does something you have to correct, ask whether it could have known. If it could\nhave, and the guide file did not say so, add a line. If it could not have, that is a codebase problem\nand the fix is in the code.</p>\n\n<p>After a few weeks this converges on a document that is short, specific to your project, and mostly\ncomposed of things that were genuinely surprising once.</p>\n\n<h2 id=\"the-structure-that-works\">The structure that works</h2>\n\n<ol>\n<li>What this project is, in two sentences.</li>\n<li>The rule that must never be broken.</li>\n<li>The shape of a feature, pointing at a real example.</li>\n<li>Where the source of truth lives for routes, schema, and integrations.</li>\n<li>How to run things, including the one verification command.</li>\n<li>What is already built, so it does not get rebuilt.</li>\n<li>The conventions worth stating, especially the silent traps.</li>\n</ol>\n\n<p>That fits comfortably in 150 lines and does more than a document four times the length.</p>\n\n<h2 id=\"the-part-people-miss\">The part people miss</h2>\n\n<p>A guide file is only half of it. The other half is that the codebase has to actually be followable.</p>\n\n<p>If your project does the same thing three different ways, no document will fix that, because the\nagent reads the code too and the code disagrees with itself. Write the rule, then make the codebase\nmatch it. The document describes the grain; it cannot create one.</p>\n\n<p>Related: <a href=\"/blog/where-the-first-run-goes\" rel=\"noopener\">why the first agent run disappears into plumbing</a> and <a href=\"/blog/briefing-your-agent\" rel=\"noopener\">how\nto brief an agent for one working feature</a>.</p>\n","date_published":"2026-08-02T00:00:00+00:00","date_modified":"2026-08-02T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["agents","craft"]},{"id":"https://shiponeshot.com/blog/briefing-your-agent","url":"https://shiponeshot.com/blog/briefing-your-agent","title":"How do you brief a coding agent to get one working feature?","summary":"Say what it is, say what it is not, name the example to copy. Three sentences beats three paragraphs, and over-specifying makes the result worse.","content_html":"<p>How do you brief a coding agent to get one working feature?</p>\n\n<p>Three sentences: what the thing is, what it is explicitly not, and which existing example to copy.\nEverything else should already live in the codebase, and if it does not, the fix is to put it there\nrather than to write a longer prompt. Long briefs feel thorough and mostly produce features with\nparts nobody wanted.</p>\n\n<h2 id=\"the-three-sentence-brief\">The three-sentence brief</h2>\n\n<p>Here is a complete, real one:</p>\n<pre class=\"highlight\"><code>Add a Bookmarks feature: a url, a title, and an optional note.\nNo tags, no folders, no search yet.\nFollow the shape of the example slice, and put it behind the paywall.\n</code></pre>\n<p>Sentence one is the thing. Sentence two is the boundary. Sentence three points at a template and\nmakes the one product decision the agent cannot infer.</p>\n\n<p>That is enough to get back a model, a migration, permission rules, a controller, screens, routes, and\ntests, all consistent with everything else in the codebase.</p>\n\n<h2 id=\"why-sentence-two-matters-most\">Why sentence two matters most</h2>\n\n<p>\"No tags, no folders, no search yet\" is the highest-value line in that prompt, and it is the one\npeople never write.</p>\n\n<p>Given an under-specified request, an agent does not stop and ask. It fills the gap with reasonable\ndefaults, and reasonable defaults for \"bookmarks\" include tags, folders, search, import, and export,\nbecause that is what bookmarks usually have. Every one of those arrives as code you now own,\nmaintain, test, and eventually remove.</p>\n\n<p>Stating the boundary is cheaper than stating the contents. You can describe what you want in eight\nwords; describing everything you do not want would take a page. So name the two or three obvious\nextensions and rule them out explicitly.</p>\n\n<h2 id=\"why-over-specifying-makes-it-worse\">Why over-specifying makes it worse</h2>\n\n<p>The instinct after a bad result is to write a longer prompt. It usually makes things worse, for three\nreasons.</p>\n\n<p><strong>It competes with the codebase.</strong> If your guide file says one thing and your prompt implies another,\nyou have created a conflict, and which one wins is not predictable.</p>\n\n<p><strong>It buries the important part.</strong> In a page of instructions, the one line that actually mattered gets\nthe same weight as the rest.</p>\n\n<p><strong>It specifies the wrong layer.</strong> People over-specify implementation (\"use a join table with a\ncomposite index\") and under-specify product (\"should two people be able to bookmark the same thing\").\nThe implementation is the part the agent is good at. The product decisions are the part only you\nknow, and they are what the brief should spend its words on.</p>\n\n<h2 id=\"put-the-repeated-parts-in-the-codebase-not-the-prompt\">Put the repeated parts in the codebase, not the prompt</h2>\n\n<p>If you find yourself typing the same instruction every time, that is a signal.</p>\n\n<p>\"Make sure it is scoped to the account, add permission checks, write tests, use the existing design\nclasses\" should not be in a prompt. It should be in the guide file, and it should be visible in the\nworked example the agent copies. Then it happens whether you remember to ask or not, which is the\nentire point.</p>\n\n<p>A prompt is for what is different about <em>this</em> feature. Everything that is true of every feature\nbelongs somewhere permanent.</p>\n\n<h2 id=\"ask-for-a-plan-when-the-request-is-loose\">Ask for a plan when the request is loose</h2>\n\n<p>For anything you cannot state in three sentences, spend one round on a plan:</p>\n<pre class=\"highlight\"><code>Before writing code, tell me in five lines how you would add team invitations.\n</code></pre>\n<p>Five lines is enough to reveal a misunderstanding and cheap enough that you will actually read it.\nCatching a wrong assumption here costs seconds; catching it in a finished diff costs a rebuild.</p>\n\n<p>Skip this for small, well-bounded work. Use it whenever the feature touches something that already\nexists.</p>\n\n<h2 id=\"what-to-review-in-order\">What to review, in order</h2>\n\n<p>The reviewing habit matters more than the prompting habit, and there is an order that catches the\nmost for the least reading:</p>\n\n<ol>\n<li><strong>The migration.</strong> Schema is the most expensive thing to change once there is data.</li>\n<li><strong>The permission rules.</strong> Deny by default, every query filtered to the current account.</li>\n<li><strong>The lookups.</strong> A query through the model rather than through the current account is how one\ncustomer reads another's records, and the two lines look nearly identical.</li>\n<li><strong>Everything else</strong>, quickly. Screens and copy are cheap to change and rarely dangerous.</li>\n</ol>\n\n<h2 id=\"the-signal-that-your-setup-is-wrong\">The signal that your setup is wrong</h2>\n\n<p>If your briefs keep getting longer, the problem is not your prompting.</p>\n\n<p>A prompt that has to explain your conventions means your conventions are not written down. A prompt\nthat has to describe the shape of a feature means there is no example to point at. A prompt that has\nto say \"and make sure it works\" means there is no command that verifies.</p>\n\n<p>Fix those three and the briefs get shorter on their own. That is the argument in <a href=\"/blog/agent-guide-file\" rel=\"noopener\">how to write a\nguide file your agent will actually follow</a>, and it is why <a href=\"/blog/where-the-first-run-goes\" rel=\"noopener\">the first run\notherwise disappears into plumbing</a>.</p>\n\n<h2 id=\"the-one-line-version\">The one-line version</h2>\n\n<p>Say what it is. Say what it is not. Point at the example. Then read the migration and the permission\nrules properly, and skim the rest.</p>\n","date_published":"2026-08-01T00:00:00+00:00","date_modified":"2026-08-01T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["agents","craft"]},{"id":"https://shiponeshot.com/blog/safe-to-launch-checklist","url":"https://shiponeshot.com/blog/safe-to-launch-checklist","title":"What makes an app safe to launch?","summary":"Twelve checks that separate a demo from something you can put in front of strangers, ordered by how much damage each one prevents.","content_html":"<p>What makes an app safe to launch?</p>\n\n<p>Twelve things, and they are not evenly important. The first four prevent a customer seeing another\ncustomer's data, someone using what they did not pay for, an account takeover, or a leaked secret.\nThe rest prevent smaller, recoverable problems. If you only have a day before launch, do the first\nfour properly and skip the rest rather than doing all twelve badly.</p>\n\n<p>Here they are, in order of how much damage each one prevents.</p>\n\n<h2 id=\"the-four-that-actually-matter\">The four that actually matter</h2>\n\n<h3 id=\"1-every-query-is-scoped-to-its-customer\">1. Every query is scoped to its customer</h3>\n\n<p>The worst thing that can happen. One missing filter and somebody reads somebody else's data.</p>\n\n<p>Check: pick five endpoints that load a record by id and confirm each looks it up <strong>through the\ncurrent account</strong>, not through the model directly. Then confirm you have a test that requesting\nanother account's record returns a 404.</p>\n\n<p>This has no symptoms in development, because you have one account and everything you can see is\nyours. Do not rely on having noticed it. The full version is in <a href=\"/blog/keeping-customer-data-separate\" rel=\"noopener\">keeping every customer's data\nseparate</a>.</p>\n\n<h3 id=\"2-paid-features-are-checked-on-the-server\">2. Paid features are checked on the server</h3>\n\n<p>Hiding a link is not access control. If the only thing between an unpaid account and a paid feature\nis an unrendered button, the feature is free to anyone who types the address.</p>\n\n<p>Check: sign in with an account that has no subscription and request a paid endpoint directly. You\nshould be refused.</p>\n\n<h3 id=\"3-no-secrets-in-the-repository\">3. No secrets in the repository</h3>\n\n<p>An API key committed once is compromised forever, because it is in the history even after you delete\nit. This includes test keys, which are frequently attached to accounts that can do real things.</p>\n\n<p>Check: run an automated secret scanner over your <strong>entire history</strong>, not just the current files.\nEveryone does this eventually. Do it before strangers can read the code.</p>\n\n<h3 id=\"4-sign-in-cannot-be-brute-forced-or-abused\">4. Sign-in cannot be brute-forced or abused</h3>\n\n<p>Codes and tokens are credentials. They need short expiry, a limited number of attempts, and rate\nlimits both per account and per network address, so nobody can request fifty sign-in codes for\nsomeone else's inbox.</p>\n\n<p>Check: request the same thing twenty times quickly and confirm you get refused.</p>\n\n<h2 id=\"the-eight-that-follow\">The eight that follow</h2>\n\n<h3 id=\"5-rate-limiting-on-the-endpoints-that-hurt\">5. Rate limiting on the endpoints that hurt</h3>\n\n<p>Sign-in, sign-up, password or code requests, checkout, and anything that sends email or costs you\nmoney per call. A blanket limit per address is a reasonable backstop.</p>\n\n<p>One caveat worth knowing: <strong>exclude the paths you want crawled</strong>. A blanket limit that also catches\nsearch engines and AI crawlers will hand them errors, and they read that as a reason to visit less\noften. That failure is completely invisible from inside the app.</p>\n\n<h3 id=\"6-a-content-security-policy-actually-enforced\">6. A content security policy, actually enforced</h3>\n\n<p>Not report-only. Report-only tells you about problems and prevents none of them.</p>\n\n<p>The warning: an enforced policy <strong>fails silently</strong>. When it blocks something, the browser does not\nraise an error your app can see. The button just does nothing. So test the flows that leave your\nsite, especially checkout and social sign-in, with the policy on. Finding this in production is a\nmiserable afternoon.</p>\n\n<h3 id=\"7-webhooks-verify-signatures\">7. Webhooks verify signatures</h3>\n\n<p>Any endpoint that changes state based on an outside call must verify it came from who it claims.\nWithout this, your payment webhook is a public URL that grants subscriptions to anyone who finds it,\nwith no error anywhere. See <a href=\"/blog/charging-for-your-app\" rel=\"noopener\">charging for your app</a>.</p>\n\n<h3 id=\"8-data-export-and-deletion-work\">8. Data export and deletion work</h3>\n\n<p>In several jurisdictions people have a right to their data and a right to have it removed, on a\ndeadline. Beyond the legal question, \"delete my account\" is a request you will get in the first week.</p>\n\n<p>Check: actually run both, end to end, on a real account. Deletion in particular tends to fail on the\nrecords nobody remembered.</p>\n\n<h3 id=\"9-email-is-authenticated\">9. Email is authenticated</h3>\n\n<p>SPF, DKIM, and DMARC published, and your From address on the domain you signed with. If you use\npasswordless sign-in, this is not a deliverability concern, it is an availability concern: mail in\nspam means nobody can log in. See <a href=\"/blog/email-that-arrives\" rel=\"noopener\">email that reaches the inbox</a>.</p>\n\n<h3 id=\"10-backups-exist-and-have-been-restored\">10. Backups exist and have been restored</h3>\n\n<p>A backup you have never restored is a hypothesis. Restore one into a scratch environment and confirm\nthe data is there. Do this once before launch and you will sleep better forever.</p>\n\n<h3 id=\"11-errors-go-somewhere-you-will-see\">11. Errors go somewhere you will see</h3>\n\n<p>Not just log files on a server nobody opens. You want to find out about the exception from a\nnotification, not from the customer who hit it.</p>\n\n<h3 id=\"12-dependencies-have-been-audited\">12. Dependencies have been audited</h3>\n\n<p>One command tells you whether anything you depend on has a known vulnerability. Run it, and run it on\nevery change after.</p>\n\n<h2 id=\"make-it-one-command\">Make it one command</h2>\n\n<p>The checklist above is worth doing once. It is worth much more if it cannot rot.</p>\n\n<p>Most of it can be automated: tests, a linter, a static security scan, a dependency audit, and a secret\nscan behind a single command that exits non-zero if anything fails. Then \"is it safe to ship?\"\nbecomes a thing you run rather than a thing you remember, and it stays true as the codebase grows.</p>\n\n<p>That is also what makes a coding agent's work verifiable. An agent with one unambiguous command can\ncheck itself and fix what it broke before you ever see the diff. Without one, \"done\" is something it\nasserts. That is most of the argument in <a href=\"/blog/agent-guide-file\" rel=\"noopener\">how to write a guide file your agent will actually\nfollow</a>.</p>\n\n<h2 id=\"what-is-not-on-this-list\">What is not on this list</h2>\n\n<p>Some things that feel urgent and are not, before you have customers:</p>\n\n<ul>\n<li><strong>Load testing.</strong> You do not have load. A small server handles more than you think.</li>\n<li><strong>A staging environment.</strong> Useful later, overhead now.</li>\n<li><strong>Multi-region anything.</strong></li>\n<li><strong>A status page.</strong></li>\n</ul>\n\n<p>Do the first four. Automate as much of the rest as you can. Then launch, because the fastest way to\nlearn what actually matters is to have real customers using it.</p>\n","date_published":"2026-07-31T00:00:00+00:00","date_modified":"2026-07-31T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["security","how-to","launch"]},{"id":"https://shiponeshot.com/blog/background-work-without-extra-services","url":"https://shiponeshot.com/blog/background-work-without-extra-services","title":"Do you need Redis to run background jobs?","summary":"No. A queue on your existing database removes a service to run, secure, and pay for. The real tradeoff, and four rules that make jobs reliable.","content_html":"<p>Do you need Redis to run background jobs?</p>\n\n<p>No. For most products a queue that runs on your existing database is the better choice: one less\nservice to run, secure, back up, and pay for, and jobs that participate in the same transactions as\nyour data. The historic reason to reach for a separate in-memory store was throughput, and the\nthreshold where that actually matters is far higher than most products ever reach.</p>\n\n<p>Here is the real tradeoff and what actually makes jobs reliable, which is not the queue you picked.</p>\n\n<h2 id=\"what-background-jobs-are-for\">What background jobs are for</h2>\n\n<p>Anything that should not happen while somebody waits for a page:</p>\n\n<ul>\n<li><strong>Sending email.</strong> A slow mail provider should never slow down a request.</li>\n<li><strong>Calling other people's APIs</strong>, which can be slow or down.</li>\n<li><strong>Processing uploads.</strong></li>\n<li><strong>Scheduled work</strong>: nightly cleanups, daily digests, expiring records.</li>\n<li><strong>Anything that might need retrying</strong>, which is most things that touch a network.</li>\n</ul>\n\n<p>The rule of thumb: if it can fail independently of the request, or take longer than the person is\nwilling to wait, it belongs in a job.</p>\n\n<h2 id=\"the-case-for-the-database-backed-queue\">The case for the database-backed queue</h2>\n\n<p><strong>One less service.</strong> A separate queue store is a thing to install, configure, secure, monitor, back\nup, upgrade, and pay for. Removing it removes all of that at once, and at small scale it is a\nmeaningful share of your operational surface.</p>\n\n<p><strong>Jobs and data stay consistent.</strong> This is the technically interesting advantage. If enqueuing a job\nis a write to the same database as the change that triggered it, both happen in the same transaction.\nEither the order is created and the confirmation email is queued, or neither is.</p>\n\n<p>With a separate store you get the classic split-brain: the database write succeeds and the enqueue\nfails, so the order exists and nobody is told. Or the enqueue succeeds and the transaction rolls\nback, so a job runs for an order that does not exist. Both are avoidable, and avoiding them takes\ncare you would rather spend elsewhere.</p>\n\n<p><strong>Jobs survive.</strong> They are rows. A restart does not lose them, and you can query them with the tools\nyou already have.</p>\n\n<p><strong>You can inspect it.</strong> Debugging a stuck queue is a query, not a specialised client.</p>\n\n<h2 id=\"the-honest-case-against\">The honest case against</h2>\n\n<p><strong>Throughput has a ceiling.</strong> A separate in-memory store handles far more jobs per second. If you are\nprocessing tens of thousands per minute, you will feel the difference.</p>\n\n<p><strong>Polling has a cost.</strong> A database queue checks for work periodically rather than being pushed to,\nwhich means a small constant load and a small latency floor. Neither is noticeable at ordinary\nvolumes.</p>\n\n<p><strong>Very large backlogs are heavier.</strong> A queue table with millions of rows needs attention in a way a\nsmaller one does not.</p>\n\n<p>The honest summary: the crossover point is far above where most products live, and the operational\nsaving is immediate. Start with the database, and if you ever genuinely outgrow it, you will have\nreal numbers to make the change with.</p>\n\n<h2 id=\"the-four-rules-that-actually-matter\">The four rules that actually matter</h2>\n\n<p>The queue is the least important decision here. These are what determine whether your jobs work.</p>\n\n<h3 id=\"1-jobs-must-be-safe-to-run-twice\">1. Jobs must be safe to run twice</h3>\n\n<p>The most important rule, and the most commonly broken.</p>\n\n<p>Jobs get retried. A worker dies mid-run, a network call times out, a deploy interrupts things. A job\nthat ran halfway and gets retried will run its first half again.</p>\n\n<p>So \"send the welcome email\" must not send two. \"Charge the customer\" must not charge twice. The usual\nfixes are to record that the work was done and check that record first, or to write the operation so\nthat doing it again changes nothing after the first time.</p>\n\n<p>This is the same property <a href=\"/blog/charging-for-your-app\" rel=\"noopener\">payment webhooks need</a>, for the same reason,\nand it is worth internalising once: <strong>anything that can be delivered more than once will be.</strong></p>\n\n<h3 id=\"2-pass-ids-not-objects\">2. Pass ids, not objects</h3>\n\n<p>Enqueue the identifier and load the record inside the job.</p>\n\n<p>If you serialize a whole object into the queue, the job runs against a snapshot from whenever it was\nenqueued, which may be minutes or hours stale. Worse, if the record was deleted in the meantime, you\nare operating on something that no longer exists. Load it fresh, and handle the case where it is\ngone.</p>\n\n<h3 id=\"3-assume-the-job-will-fail\">3. Assume the job will fail</h3>\n\n<p>Not might. Will. Networks fail, providers have bad minutes, records disappear.</p>\n\n<p>Configure retries with increasing delays, decide what happens after the last one, and make sure\nfailures are visible somewhere you will actually look. A queue that silently drops work is worse than\nno queue, because you believe the work happened.</p>\n\n<h3 id=\"4-scope-jobs-to-their-account-explicitly\">4. Scope jobs to their account explicitly</h3>\n\n<p>Easy to miss and important. A job runs with no request and no current account, so all the automatic\nscoping that protects your controllers does not apply.</p>\n\n<p>Pass the account identifier into the job and scope every query inside it explicitly. This is one of\nthe <a href=\"/blog/keeping-customer-data-separate\" rel=\"noopener\">places tenant isolation usually gets\nmissed</a>, precisely because the surrounding machinery is absent.</p>\n\n<h2 id=\"scheduled-work\">Scheduled work</h2>\n\n<p>Recurring jobs, nightly cleanups, digests. Two things to watch.</p>\n\n<p><strong>Overlap.</strong> If a job runs every five minutes and one run takes six, you now have two running at\nonce. Decide whether that is safe, and if not, prevent it.</p>\n\n<p><strong>Duplication across servers.</strong> If you ever run more than one application server, each will try to\nrun the schedule. Make sure exactly one does.</p>\n\n<h2 id=\"the-summary\">The summary</h2>\n\n<ul>\n<li>You do not need a separate service for background jobs. A database-backed queue is a real answer,\nnot a compromise.</li>\n<li>It removes an entire operational dependency and keeps jobs consistent with your data.</li>\n<li>Reconsider only when you have measured throughput that genuinely needs more.</li>\n<li>The queue matters far less than the four rules: safe to run twice, pass ids, expect failure, scope\nexplicitly.</li>\n</ul>\n\n<p>Fewer moving parts is a feature. It is the same logic that makes <a href=\"/blog/going-live-on-your-own-server\" rel=\"noopener\">one small server the right\nanswer</a> for longer than people expect.</p>\n","date_published":"2026-07-29T00:00:00+00:00","date_modified":"2026-07-29T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["jobs","how-to"]},{"id":"https://shiponeshot.com/blog/building-with-hermes","url":"https://shiponeshot.com/blog/building-with-hermes","title":"What is it like to build an app with Hermes?","summary":"Hermes keeps context as you go and runs the same core wherever you drive it from. Here is what that changes about a build session, and what to set up first.","content_html":"<p>What is it like to build with Hermes across terminal, chat, and desktop?</p>\n\n<p>The distinctive part is continuity. You can start a change in your terminal, ask a follow-up question\nfrom a chat window, and pick it up again later without re-explaining what you are building. That\nmatters most on the second and third weeks of a project, when the codebase has grown past what fits\nin one conversation and the expensive thing is no longer writing code but re-establishing context.</p>\n\n<p>What it does not change is the need for that context to be written down somewhere the agent can read.</p>\n\n<h2 id=\"continuity-is-not-a-substitute-for-a-guide-file\">Continuity is not a substitute for a guide file</h2>\n\n<p>This is the trap worth naming early.</p>\n\n<p>When an agent remembers your project, it is tempting to stop writing anything down and just tell it\nthings. That works until the moment it does not: a new session, a different surface, a teammate, or\nsimply a conversation long enough that the early part stops mattering.</p>\n\n<p>A written guide file is different from remembered context in one important way: it is the same for\nevery session, and you can review it. If the rule \"every query is scoped to the account\" lives only\nin a conversation, you cannot audit it, and you will not notice when it stops being followed.</p>\n\n<p>So use the continuity for what it is good at, which is the running state of what you are building,\nand keep the invariants in a file.</p>\n\n<h2 id=\"the-setup\">The setup</h2>\n\n<p>Same as everywhere, and short:</p>\n\n<ol>\n<li><strong>A guide file</strong> stating the rules that must hold no matter what.</li>\n<li><strong>A worked example</strong> of the shape you want copied.</li>\n<li><strong>One command that verifies</strong>, so \"done\" is checkable.</li>\n</ol>\n\n<p>With those in place, a session that starts from any surface begins in roughly the same position.</p>\n\n<h2 id=\"what-a-session-looks-like\">What a session looks like</h2>\n\n<p>The request is ordinary:</p>\n<pre class=\"highlight\"><code>Add a waitlist: an email address, a source, and a timestamp.\nFree feature, not behind the paywall.\n</code></pre>\n<p>Two things are doing work in that prompt. \"Free feature, not behind the paywall\" is a product\ndecision the agent cannot infer, so you supply it. Everything else it can find: that a waitlist entry\nbelongs to an account, that queries go through the current account, that a resource ships with tests,\nall of that is in the codebase already.</p>\n\n<p>The output is the same shape as any other feature here: model, migration, permission policy,\ncontroller, screens, route, tests. You review the migration and the policy carefully and the rest\nquickly.</p>\n\n<h2 id=\"where-the-continuity-actually-pays\">Where the continuity actually pays</h2>\n\n<p>Three places, concretely.</p>\n\n<p><strong>The second feature.</strong> When you ask for something that relates to what you built last week, it\nalready knows the shape you chose and does not propose a different one.</p>\n\n<p><strong>Debugging across sessions.</strong> \"That thing we changed on Tuesday broke sign-in\" is a usable sentence\nrather than a research project.</p>\n\n<p><strong>Asking from wherever you are.</strong> The most common real use is not building at all, it is asking a\nquestion about your own codebase from a phone. \"What decides whether an account can use the paid\nfeature?\" is a question with an answer, and getting it without opening a laptop is genuinely useful.</p>\n\n<h2 id=\"where-it-goes-wrong\">Where it goes wrong</h2>\n\n<p><strong>Remembered context drifts.</strong> Something true in week one stops being true in week three, and unless\nyou correct it, it keeps being applied. Periodically re-state the important invariants and, better,\nput them in the guide file where they cannot rot silently.</p>\n\n<p><strong>Convenience encourages vagueness.</strong> Because asking is easy, requests get shorter, and short\nrequests are ambiguous requests. The agent fills ambiguity with reasonable defaults, and reasonable\ndefaults are how you end up with a feature nobody asked for.</p>\n\n<p><strong>Verification is easy to skip from a chat surface.</strong> Running the test suite from a terminal is\nnatural. Remembering to run it after a change you made from a message thread is less so. Decide that\nnothing counts as done until the checks pass, regardless of where the change came from.</p>\n\n<h2 id=\"what-it-does-not-change\">What it does not change</h2>\n\n<p>None of the <a href=\"/blog/what-your-agent-should-not-build\" rel=\"noopener\">four categories where generated code is plausibly\nwrong</a> get safer because the agent remembers your project.\nSign-in, payments, email deliverability, and customer data separation are best handled by already\nhaving them, tested, before the first prompt. That is <a href=\"/blog/what-a-starter-kit-is\" rel=\"noopener\">the argument for starting from a\nkit</a>.</p>\n\n<p>The same loop with other agents: <a href=\"/blog/building-with-claude-code\" rel=\"noopener\">Claude Code</a>,\n<a href=\"/blog/building-with-codex\" rel=\"noopener\">Codex</a>, <a href=\"/blog/building-in-cursor\" rel=\"noopener\">Cursor</a>, and\n<a href=\"/blog/building-with-openclaw\" rel=\"noopener\">OpenClaw</a>.</p>\n","date_published":"2026-07-28T00:00:00+00:00","date_modified":"2026-07-28T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["agents","hermes","walkthrough"]},{"id":"https://shiponeshot.com/blog/charging-for-your-app","url":"https://shiponeshot.com/blog/charging-for-your-app","title":"How do you charge for your app without building billing?","summary":"The checkout is the easy half. The whole flow, why the webhook is the source of truth, and four failures that let people use what they never paid for.","content_html":"<p>How do you charge for your app without building a billing system?</p>\n\n<p>Use a payment provider's hosted checkout, and treat their webhook as the only source of truth about\nwhat someone has paid for. That is the whole architecture. The mistake that costs people real money\nis thinking the checkout is the feature, when the checkout is perhaps a fifth of it. The other four\nfifths is keeping your idea of who has paid in sync with reality, forever, without anyone at a\nkeyboard.</p>\n\n<p>Here is the complete flow and the four ways it breaks.</p>\n\n<h2 id=\"the-flow-end-to-end\">The flow, end to end</h2>\n\n<ol>\n<li>Someone clicks \"Subscribe\".</li>\n<li>Your server creates a checkout session with the provider and redirects them to it.</li>\n<li>They pay on the provider's page. <strong>Their card details never touch your servers</strong>, which is the\nsingle biggest reason to do it this way.</li>\n<li>The provider redirects them back to a success page.</li>\n<li>Separately, the provider sends your server a webhook saying the subscription started.</li>\n<li><strong>Your server updates its records when the webhook arrives</strong>, not when the redirect lands.</li>\n<li>Every time someone tries to use a paid feature, you check those records.</li>\n</ol>\n\n<p>Steps 5 and 6 are the ones people collapse into step 4, and that is where the trouble starts.</p>\n\n<h2 id=\"why-the-webhook-is-the-source-of-truth\">Why the webhook is the source of truth</h2>\n\n<p>The redirect is a courtesy. The webhook is the fact.</p>\n\n<p>Consider what happens when the redirect does not arrive. The customer pays and closes the tab. Their\nphone loses signal at the moment of return. The browser crashes. A corporate proxy eats it.</p>\n\n<p>In every one of those cases the money has moved and the customer has nothing, because your only\nrecord of the payment was a page they never loaded. They will write to support, correctly annoyed,\nand you will fix it by hand.</p>\n\n<p>The webhook has none of those failure modes. It is a server-to-server call from the payment provider,\nretried on failure until it succeeds. Build on it, and use the success page for nothing more than\nsaying thank you.</p>\n\n<h2 id=\"the-four-failures\">The four failures</h2>\n\n<h3 id=\"1-not-verifying-the-signature\">1. Not verifying the signature</h3>\n\n<p>Your webhook endpoint is a public URL that writes to your database. Without signature verification,\nanyone who finds it can post a message saying \"this account has an active subscription\", and your\napp will believe it.</p>\n\n<p>There is no error state for this. It works exactly as designed, for everyone, including people who\nhave not paid. Every provider signs its webhooks and gives you a secret to check against. Use it, and\nreject anything that fails.</p>\n\n<h3 id=\"2-not-handling-the-same-event-twice\">2. Not handling the same event twice</h3>\n\n<p>Payment providers retry. If your endpoint is slow, or returns an error, or the network hiccups, the\nsame event arrives again. This is not an edge case, it is the documented design, and it happens most\nduring outages when retries pile up.</p>\n\n<p>So handling an event twice has to do exactly what handling it once did. Record the event id and skip\nones you have seen, or write the update so that applying it repeatedly changes nothing after the\nfirst time. Either works. Neither is optional.</p>\n\n<h3 id=\"3-storing-paid-as-a-boolean\">3. Storing \"paid\" as a boolean</h3>\n\n<p><code>account.paid = true</code> seems reasonable on the day of purchase and is wrong by the end of the month.</p>\n\n<p>Subscriptions change state on their own: they renew, they fail to renew, they get cancelled at the\nend of a period, they get refunded, they get disputed. What you need to store is the current state\nand the date it runs until, and then answer \"may this account use the paid feature?\" by consulting\nboth.</p>\n\n<p>The concrete version: store the status and the period end, and let the webhook keep them current. Do\nnot compute access once and cache it in a boolean nobody updates.</p>\n\n<h3 id=\"4-checking-entitlement-in-the-interface-only\">4. Checking entitlement in the interface only</h3>\n\n<p>Hiding the button is not access control. If the only thing stopping an unpaid account from using a\npaid feature is that the link is not rendered, then the feature is available to anyone who types the\naddress.</p>\n\n<p>The check belongs on the server, on the request path, in one place that every paid feature shares.\nOne line in a controller, not a condition scattered through your screens.</p>\n\n<h2 id=\"subscriptions-or-one-time-payments\">Subscriptions or one-time payments?</h2>\n\n<p>Briefly, because it changes what you build.</p>\n\n<p><strong>One-time</strong> is simpler: it either happened or it did not, and there is no ongoing state to track.</p>\n\n<p><strong>Subscriptions</strong> are more work because of everything in failure three, but they are what most\nproducts need, and the machinery you build is the same machinery you would need for renewals anyway.</p>\n\n<p><strong>Usage-based</strong> is a different problem entirely, involving metering and reporting, and it is worth\nbeing sure you need it before taking it on.</p>\n\n<h2 id=\"charging-inside-a-mobile-app\">Charging inside a mobile app</h2>\n\n<p>If your product has an iPhone or Android app and you sell digital goods, both stores require you to\nuse their in-app purchase system, and both take a cut of up to 30%.</p>\n\n<p>This is not a technical detail, it is the largest line item in <a href=\"/blog/what-it-costs-to-start\" rel=\"noopener\">what it costs to run an\napp</a>. It also means a second purchase path with its own receipts and\nits own webhooks, arriving at the same question: is this account currently entitled?</p>\n\n<p>The way to keep that manageable is to make entitlement a single concept in your codebase that both\npaths write to, rather than two parallel systems that disagree. There is more on that in <a href=\"/blog/your-app-in-the-app-stores\" rel=\"noopener\">getting\nyour web app into the app stores</a>.</p>\n\n<h2 id=\"how-to-test-any-of-this\">How to test any of this</h2>\n\n<p>The thing that makes billing miserable to work on is that exercising it usually means real money, or\na test-mode account and a lot of clicking.</p>\n\n<p>The alternative is a seam: a real client when credentials are present, and a deterministic local\nstand-in when they are not. The same code path, a different implementation behind it. That means the\nentire purchase flow runs in process on your laptop, and your test suite can exercise subscribe,\ncancel, and expire without touching the network.</p>\n\n<p>It is also what lets a new developer, or a coding agent, run and modify the billing flow on the first\nday without anyone handing them a key.</p>\n\n<h2 id=\"the-checklist\">The checklist</h2>\n\n<ul>\n<li>Hosted checkout, so card details never reach your servers.</li>\n<li>The webhook is the source of truth. The redirect is a thank-you page.</li>\n<li><strong>Verify signatures.</strong> Reject anything unsigned.</li>\n<li><strong>Handle duplicate delivery.</strong> The same event twice must change nothing the second time.</li>\n<li>Store <strong>status and period end</strong>, not a boolean.</li>\n<li>Check entitlement <strong>on the server</strong>, in one shared place.</li>\n<li>Have a local stand-in so the flow is testable without money.</li>\n</ul>\n\n<p>This is one of the <a href=\"/blog/what-your-agent-should-not-build\" rel=\"noopener\">four things worth not building\nyourself</a>, because a webhook that skips signature\nverification looks exactly like one that does not.</p>\n","date_published":"2026-07-27T00:00:00+00:00","date_modified":"2026-07-27T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["payments","how-to"]},{"id":"https://shiponeshot.com/blog/sign-in-without-passwords","url":"https://shiponeshot.com/blog/sign-in-without-passwords","title":"Should you build sign-in without passwords?","summary":"Emailed codes and social sign-in remove the password database and the reset flow. What you gain, what you take on, and the detail everyone gets wrong.","content_html":"<p>Should you build sign-in without passwords?</p>\n\n<p>For most new products, yes. Dropping passwords removes four things you would otherwise own forever:\na password database worth stealing, a reset flow, the rules about length and reuse, and the support\nrequests that come from all three. You replace them with two dependencies, email delivery and the\nidentity providers you accept, and one detail that almost everyone gets wrong the first time.</p>\n\n<p>Here is the trade in full, including the part that bites.</p>\n\n<h2 id=\"what-passwordless-actually-means\">What \"passwordless\" actually means</h2>\n\n<p>Two mechanisms, usually together:</p>\n\n<p><strong>A code by email.</strong> Someone types their address, you email a short code, they type it back. There is\nnothing to remember and nothing stored that is worth stealing.</p>\n\n<p><strong>Social sign-in.</strong> Continue with Google or Apple. The provider proves who they are, and you never\nhandle a credential at all.</p>\n\n<p>Offering both covers nearly everyone: people who have a Google or Apple account use the button, and\neveryone else uses their email.</p>\n\n<h2 id=\"what-you-gain\">What you gain</h2>\n\n<p><strong>No password database.</strong> The single most valuable property. A stolen password file is a breach that\nfollows your customers to every other site where they reused that password. If you never had one,\nthat entire class of incident cannot happen to you.</p>\n\n<p><strong>No reset flow.</strong> Password reset is a small feature with a large surface: token generation,\nexpiry, single use, and a set of enumeration questions about whether \"no account with that address\"\nis a safe thing to say. Passwordless sign-in and password reset are nearly the same flow, so you\nbuild it once instead of twice.</p>\n\n<p><strong>Less support.</strong> \"I cannot log in\" is one of the highest-volume support categories for any product,\nand most of it is password-shaped.</p>\n\n<p><strong>Better completion.</strong> Fewer fields and nothing to remember, on a phone especially.</p>\n\n<h2 id=\"what-you-take-on\">What you take on</h2>\n\n<p>Be honest about these, because they are real.</p>\n\n<p><strong>Your sign-in now depends on email delivery.</strong> If your mail lands in spam, nobody can get in. This\nturns <a href=\"/blog/email-that-arrives\" rel=\"noopener\">deliverability</a> from a marketing concern into an availability\nconcern, and it is the strongest argument for setting up your DNS records properly before launch\nrather than after.</p>\n\n<p><strong>Sign-in gets slower.</strong> Switching to an inbox and back is more work than a saved password in a\nbrowser. Social sign-in avoids this for the people who use it, which is a good reason to offer both.</p>\n\n<p><strong>You depend on identity providers.</strong> If someone loses access to their Google account, they lose\naccess to yours. Accepting email codes as well as social sign-in gives them a route back.</p>\n\n<p><strong>Codes are a credential.</strong> They need the same care as a password: hashed at rest, short expiry, a\nlimited number of attempts, and rate limits per address and per network. A six-digit code with\nunlimited guesses is a four-digit problem.</p>\n\n<h2 id=\"the-detail-everyone-gets-wrong\">The detail everyone gets wrong</h2>\n\n<p>Here is the one, and it is worth reading twice.</p>\n\n<p>Somebody signs up with <code>sam@example.com</code> using an emailed code. Two months later they come back and\nclick \"Continue with Google\", which is the same address.</p>\n\n<p>Do they get their existing account, or a brand new empty one?</p>\n\n<p>Get this wrong and you create a silent duplicate. The customer sees an empty account, assumes their\ndata is gone, and writes to support saying they lost everything. Meanwhile you now have two account\nrecords for one human, possibly two subscriptions, and a reconciliation job to do by hand.</p>\n\n<p>The fix is structural, not clever: <strong>every route into the system creates or finds an account through\nexactly one function.</strong> Email codes call it. Google calls it. Apple calls it. Anything you add later\ncalls it. That function looks up by email address and decides once.</p>\n\n<p>The reason to do it this way rather than \"carefully, in three places\" is that three places is how it\nbreaks. Each provider gets added at a different time by a different person, and the third one forgets.</p>\n\n<p>One chokepoint. Every time.</p>\n\n<h3 id=\"the-verification-wrinkle\">The verification wrinkle</h3>\n\n<p>There is a subtlety underneath that, and it matters for security rather than convenience.</p>\n\n<p>Linking accounts by email address is only safe if you trust that the provider verified the address.\nGoogle and Apple do. An identity provider that lets someone claim an arbitrary unverified email would\nlet an attacker take over an existing account by signing in with its address.</p>\n\n<p>So: link on email, but only when the address is verified, and treat \"verified\" as something you check\nrather than assume.</p>\n\n<h2 id=\"what-good-looks-like\">What good looks like</h2>\n\n<p>A checklist you can hold your implementation against:</p>\n\n<ul>\n<li>Codes are <strong>stored hashed</strong>, never in plain text, and never written to a log.</li>\n<li>Codes <strong>expire quickly</strong>, in minutes, not hours.</li>\n<li>A <strong>limited number of attempts</strong>, then the code is dead.</li>\n<li><strong>Rate limits</strong> per email address and per network address, so nobody can request fifty codes for\nsomeone else's inbox.</li>\n<li><strong>One account-creation chokepoint</strong> shared by every sign-in route.</li>\n<li><strong>Email linking only on verified addresses.</strong></li>\n<li><strong>Sessions invalidated server-side</strong> on sign out, not just the cookie dropped.</li>\n<li>Both <strong>email and social</strong> offered, so neither is a single point of failure.</li>\n</ul>\n\n<h2 id=\"when-passwords-still-make-sense\">When passwords still make sense</h2>\n\n<p>Two cases, mainly.</p>\n\n<p><strong>Your customers cannot receive email reliably.</strong> Shared inboxes, locked-down corporate mail, or a\ncontext where checking email mid-task is genuinely disruptive.</p>\n\n<p><strong>Enterprise requirements.</strong> Larger customers may want single sign-on through their own identity\nprovider, which is a different project. The good news is that a codebase built around one\naccount-creation chokepoint is the easy version of adding it later.</p>\n\n<h2 id=\"the-summary\">The summary</h2>\n\n<p>Passwordless sign-in trades a password database and a reset flow for a dependency on email delivery.\nFor most products that is a clearly good trade, provided you do two things: take\n<a href=\"/blog/email-that-arrives\" rel=\"noopener\">deliverability</a> seriously, because sign-in now depends on it, and route\nevery sign-in method through one account-creation function so that the same person always gets the\nsame account.</p>\n\n<p>This is one of the <a href=\"/blog/what-your-agent-should-not-build\" rel=\"noopener\">four things worth not building\nyourself</a>, precisely because the account-merge bug looks like\nworking code right up until it does not.</p>\n","date_published":"2026-07-26T00:00:00+00:00","date_modified":"2026-07-26T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["sign-in","how-to"]},{"id":"https://shiponeshot.com/blog/going-live-on-your-own-server","url":"https://shiponeshot.com/blog/going-live-on-your-own-server","title":"How do you put your app on your own server?","summary":"One command, a $5 box, and a container. What running your own server involves in 2026, what it costs, and when a managed platform is the better answer.","content_html":"<p>How do you put your app on your own server?</p>\n\n<p>Rent a small virtual machine for <strong>$5 to $20 a month</strong>, install nothing but a container runtime, and\nuse a deploy tool that builds your app into an image, ships it to the box, and swaps it in with no\ndowntime. Setup takes an afternoon the first time and one command every time after. For most products\nbefore serious scale, this is cheaper, faster, and less surprising than a managed platform.</p>\n\n<p>Here is what is actually involved, and when you should not do it.</p>\n\n<h2 id=\"what-your-own-server-means-now\">What \"your own server\" means now</h2>\n\n<p>It does not mean a rack, and it does not mean configuring web server software by hand. The modern\nshape is:</p>\n\n<ol>\n<li>Your app builds into a <strong>container image</strong>, which contains the app and everything it needs.</li>\n<li>A <strong>deploy tool</strong> builds that image, pushes it to a registry, pulls it on the server, starts the\nnew container, waits for it to report healthy, and only then routes traffic to it.</li>\n<li>A <strong>proxy</strong> in front handles TLS certificates automatically.</li>\n</ol>\n\n<p>You do not manage the machine so much as replace what runs on it. If the box is ever damaged in a way\nyou cannot explain, the answer is to make a new one and deploy again, which takes minutes.</p>\n\n<h2 id=\"what-you-actually-need\">What you actually need</h2>\n\n<p><strong>A server.</strong> One virtual machine. Two shared cores and 2 to 4 GB of memory handles far more traffic\nthan most people expect. $5 to $20 a month.</p>\n\n<p><strong>A domain</strong>, pointed at the server. $10 to $15 a year.</p>\n\n<p><strong>A container registry</strong> to hold your images. Free tiers cover a small app comfortably.</p>\n\n<p><strong>TLS</strong>, which is free and automatic. Nobody should be paying for a certificate.</p>\n\n<p>That is the entire bill. Compare with a managed platform, where the equivalent setup with a managed\ndatabase and a worker process usually lands somewhere between $50 and $200 a month for the same\ntraffic.</p>\n\n<h2 id=\"the-database-question\">The database question</h2>\n\n<p>The one that generates the most argument, so here is the direct answer: <strong>for most products, at most\nstages, SQLite on the same server is genuinely fine.</strong></p>\n\n<p>Not a compromise, not a starter tier. Fine.</p>\n\n<p>It is fast, because there is no network between your app and its data. It is simple, because there is\nno second service to run, secure, or pay for. And modern setups handle concurrent writes far better\nthan the old reputation suggests.</p>\n\n<p>You need a separate database server when you need multiple application servers sharing one dataset,\nor when your working set genuinely outgrows one machine. Both are real, and both are much further\naway than people assume. Until then, a separate database is a bill and an operational burden that\nbuys you nothing.</p>\n\n<p>The same logic applies to your job queue and your cache. If they can live in your database rather\nthan requiring a separate service, that is one less thing to run, monitor, and pay for.</p>\n\n<h2 id=\"the-deploy-itself\">The deploy itself</h2>\n\n<p>What a good deploy does, in order:</p>\n\n<ol>\n<li>Build the image.</li>\n<li>Push it to the registry.</li>\n<li>Pull it on the server.</li>\n<li>Start the new container alongside the old one.</li>\n<li><strong>Wait for a health check to pass.</strong></li>\n<li>Switch traffic over.</li>\n<li>Stop the old container.</li>\n</ol>\n\n<p>Step five is the one that matters. Without it, a deploy that fails to boot takes your site down and\nyou find out from a customer. With it, the new container never receives traffic and the old one keeps\nserving, so a broken deploy is a non-event.</p>\n\n<h2 id=\"what-you-actually-have-to-think-about\">What you actually have to think about</h2>\n\n<p>Being honest: running your own server is not zero work. It is a small amount of specific work.</p>\n\n<p><strong>Backups.</strong> Nobody else is doing this for you. Automated, off the machine, and <strong>restored at least\nonce as a test</strong>. A backup you have never restored is a hypothesis. This is the single most important\nitem on the list and the most commonly skipped.</p>\n\n<p><strong>Operating system updates.</strong> Unattended security updates handle most of it. Reboot occasionally.</p>\n\n<p><strong>Monitoring.</strong> At minimum, something that tells you when the site is down. A free uptime checker\ncovers it.</p>\n\n<p><strong>Logs.</strong> Know where they are before you need them at two in the morning.</p>\n\n<p><strong>Secrets.</strong> Environment variables on the server, never in the repository. Somebody should check for\ncommitted secrets automatically, because everyone commits one eventually.</p>\n\n<p>That is the list. It is perhaps an hour of setup and very little ongoing attention.</p>\n\n<h2 id=\"when-a-managed-platform-is-genuinely-better\">When a managed platform is genuinely better</h2>\n\n<p>Not never. Specifically:</p>\n\n<ul>\n<li><strong>Nobody on the team wants to own a server.</strong> A platform is a real answer, and the premium buys a\nthing you would otherwise resent.</li>\n<li><strong>You need to scale to zero</strong>, or you have spiky and unpredictable traffic.</li>\n<li><strong>Compliance requires</strong> managed infrastructure with a certification you cannot produce yourself.</li>\n<li><strong>You are pre-launch and optimizing for speed</strong>, and an afternoon matters more than the monthly\ncost.</li>\n</ul>\n\n<p>Otherwise the arithmetic favors the small server, and it keeps favoring it for a long time.</p>\n\n<h2 id=\"the-thing-that-makes-this-pleasant\">The thing that makes this pleasant</h2>\n\n<p>Whatever you choose, the property worth insisting on is that <strong>deploying is one command</strong> and that it\nis safe to run at any time.</p>\n\n<p>The moment deploying becomes a checklist, people deploy less, changes pile up, and every release\nbecomes bigger and riskier than the last. That is a much more expensive problem than any hosting bill,\nand it is the real argument for setting this up properly at the start rather than \"once things are\nworking\".</p>\n\n<h2 id=\"the-summary\">The summary</h2>\n\n<ul>\n<li>One small server, $5 to $20 a month.</li>\n<li>Containers, so what runs in production is what you built.</li>\n<li>Health-checked, zero-downtime deploys, one command.</li>\n<li>Automatic TLS.</li>\n<li>SQLite on the same box until you have a concrete reason otherwise.</li>\n<li>Backups, off the machine, restored once as a test.</li>\n<li>Uptime monitoring.</li>\n</ul>\n\n<p>The costs sit alongside everything else in <a href=\"/blog/what-it-costs-to-start\" rel=\"noopener\">what it actually costs to start an\napp</a>, and the short version is that hosting is not where your money\ngoes.</p>\n","date_published":"2026-07-25T00:00:00+00:00","date_modified":"2026-07-25T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["deploy","how-to"]},{"id":"https://shiponeshot.com/blog/what-it-costs-to-start","url":"https://shiponeshot.com/blog/what-it-costs-to-start","title":"What does it actually cost to start an app in 2026?","summary":"A realistic breakdown of the money and the time: what you pay before your first customer, what is genuinely free, and where the real cost sits.","content_html":"<p>What does it actually cost to start an app in 2026?</p>\n\n<p>In money, less than most people expect: you can run a real product with paying customers for roughly\n<strong>$5 to $30 a month</strong> in infrastructure, plus payment processing of about 2.9% plus 30 cents per\ntransaction. In time, more than most people expect: <strong>four to eight weeks</strong> of building before the\nfirst customer can do anything useful, and almost all of it goes to work that is identical in every\nproduct. The time is the real cost, and it is the one worth attacking.</p>\n\n<p>Here is the breakdown behind those numbers.</p>\n\n<h2 id=\"the-money-itemized\">The money, itemized</h2>\n\n<h3 id=\"before-you-have-customers\">Before you have customers</h3>\n\n<table><thead>\n<tr>\n<th>Item</th>\n<th>Realistic cost</th>\n<th>Notes</th>\n</tr>\n</thead><tbody>\n<tr>\n<td>Domain</td>\n<td>$10 to $15 per year</td>\n<td>The one unavoidable purchase</td>\n</tr>\n<tr>\n<td>Server</td>\n<td>$5 to $20 per month</td>\n<td>A small instance handles far more than people assume</td>\n</tr>\n<tr>\n<td>Database</td>\n<td>$0</td>\n<td>SQLite on the same box is genuinely fine at this stage</td>\n</tr>\n<tr>\n<td>Background jobs</td>\n<td>$0</td>\n<td>If your queue runs on your database, there is nothing extra to pay for</td>\n</tr>\n<tr>\n<td>TLS certificate</td>\n<td>$0</td>\n<td>Automatic</td>\n</tr>\n<tr>\n<td>Email sending</td>\n<td>$0 to start</td>\n<td>Most providers have a free tier in the thousands of messages</td>\n</tr>\n<tr>\n<td>Error tracking</td>\n<td>$0 to start</td>\n<td>Free tiers are generous</td>\n</tr>\n<tr>\n<td>Payment provider</td>\n<td>$0</td>\n<td>No monthly fee, they take a cut of transactions</td>\n</tr>\n<tr>\n<td><strong>Monthly total</strong></td>\n<td><strong>$5 to $30</strong></td>\n<td></td>\n</tr>\n</tbody></table>\n\n<p>That is not a trick. A single small server running one application with a local database will serve a\nsurprising number of customers before anything needs to change.</p>\n\n<h3 id=\"the-costs-people-forget\">The costs people forget</h3>\n\n<ul>\n<li><strong>Apple Developer Program: $99 per year</strong>, required to put anything in the App Store. Google Play\nis a one-time $25.</li>\n<li><strong>Both stores take a cut</strong> of in-app purchases, currently up to 30% and lower for smaller\ndevelopers. If you sell a subscription inside a mobile app, this is a much bigger number than your\nserver bill. See <a href=\"/blog/your-app-in-the-app-stores\" rel=\"noopener\">getting your web app into the app stores</a>.</li>\n<li><strong>A business entity</strong>, if you need one, varies enormously by country.</li>\n</ul>\n\n<h3 id=\"what-gets-expensive-later-and-what-does-not\">What gets expensive later, and what does not</h3>\n\n<p>The instinct is to over-provision for scale that has not arrived. Resist it. The costs that actually\ngrow are:</p>\n\n<ul>\n<li><strong>Email volume</strong>, once you are past a free tier.</li>\n<li><strong>Anything billed per seat</strong> across your team.</li>\n<li><strong>Managed everything.</strong> A managed database, a managed queue, a managed cache, and a managed search\nindex will quietly turn $10 a month into $200 a month while serving the same number of customers.</li>\n</ul>\n\n<p>That last one is where most early budgets go, and it buys very little at a stage where you have one\nserver and one developer.</p>\n\n<h2 id=\"the-time-which-is-the-actual-cost\">The time, which is the actual cost</h2>\n\n<p>Here is where four to eight weeks goes for a competent developer building from an empty directory.\nTreat these as ranges, not promises, because they vary with experience and how much you cut.</p>\n\n<table><thead>\n<tr>\n<th>Work</th>\n<th>Typical time</th>\n</tr>\n</thead><tbody>\n<tr>\n<td>Accounts, sign-in, sessions, social login</td>\n<td>5 to 10 days</td>\n</tr>\n<tr>\n<td>Payments, webhooks, subscription state, entitlement</td>\n<td>5 to 10 days</td>\n</tr>\n<tr>\n<td>Email: provider, templates, DNS records, deliverability</td>\n<td>2 to 4 days</td>\n</tr>\n<tr>\n<td>Separating customer data, permissions</td>\n<td>3 to 6 days</td>\n</tr>\n<tr>\n<td>Background jobs</td>\n<td>1 to 3 days</td>\n</tr>\n<tr>\n<td>Security hardening, rate limits, data export and deletion</td>\n<td>3 to 5 days</td>\n</tr>\n<tr>\n<td>Deployment, TLS, CI</td>\n<td>2 to 5 days</td>\n</tr>\n<tr>\n<td><strong>Subtotal, before any of your product</strong></td>\n<td><strong>21 to 43 days</strong></td>\n</tr>\n<tr>\n<td>Your actual product</td>\n<td>however long it takes</td>\n</tr>\n</tbody></table>\n\n<p>Put a cost on that. At a contractor rate of $600 a day, the subtotal alone is $12,000 to $26,000. If\nyou are doing it yourself, it is four to eight weeks you are not spending on the thing that\ndifferentiates you, and four to eight weeks is long enough for the idea to lose its urgency.</p>\n\n<p>That is the real cost of starting an app, and it has almost nothing to do with your server bill.\nThose ranges are estimated from the outside. There is a measured version of this same table, counted\nline by line against a real codebase, in <a href=\"/blog/how-long-it-takes-to-build\" rel=\"noopener\">how long it takes to build the parts every app\nneeds</a>.</p>\n\n<h2 id=\"does-a-coding-agent-change-this\">Does a coding agent change this?</h2>\n\n<p>Partly, and less than the marketing suggests.</p>\n\n<p>An agent will absolutely produce all of the above faster than you would type it. What it will not do\nis make the review cheaper. You still have to check the four places where <a href=\"/blog/what-your-agent-should-not-build\" rel=\"noopener\">generated code looks\nright and is not</a>, and those are the slowest things to\nreview because there is no failing test to point at.</p>\n\n<p>The other thing that happens is that <a href=\"/blog/where-the-first-run-goes\" rel=\"noopener\">most of the first run disappears into\nplumbing</a> rather than into your idea, because the plumbing is what\nhas to exist first.</p>\n\n<p>So an agent compresses the twenty-one to forty-three days meaningfully. It does not remove them, and\nwhat is left is the part you least want to be responsible for.</p>\n\n<h2 id=\"the-way-to-actually-cut-it\">The way to actually cut it</h2>\n\n<p>Do not build the identical part. Start from a codebase where it already exists, is tested, and is\ndocumented, and spend your weeks on the part that is yours.</p>\n\n<p>That is what One Shot is, and it costs $299 a year at the regular price. Against a subtotal of\n$12,000 to $26,000 of equivalent work, that arithmetic is not subtle. The honest caveat is the one\nworth repeating: it is a Ruby on Rails codebase, and if nobody on your team can maintain that, none\nof this arithmetic applies to you.</p>\n\n<p>There is a full list of what that includes, and what it deliberately does not, in <a href=\"/blog/whats-in-the-box\" rel=\"noopener\">everything One\nShot gives you on day one</a>.</p>\n\n<h2 id=\"the-summary\">The summary</h2>\n\n<ul>\n<li><strong>Infrastructure is cheap</strong>: $5 to $30 a month is a real number, not a starter tier you will grow\nout of next week.</li>\n<li><strong>Mobile is the expensive channel</strong>, mostly because of store fees, not because of hosting.</li>\n<li><strong>Managed services are where budgets quietly go</strong>, and at your stage they buy very little.</li>\n<li><strong>Time is the real cost</strong>, it is four to eight weeks, and roughly all of it is work that is the same\nin every product.</li>\n</ul>\n\n<p>Attack the last line. The others largely take care of themselves.</p>\n","date_published":"2026-07-24T00:00:00+00:00","date_modified":"2026-07-24T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["basics","getting started"]},{"id":"https://shiponeshot.com/blog/whats-in-the-box","url":"https://shiponeshot.com/blog/whats-in-the-box","title":"What is One Shot, and what do you get on day one?","summary":"One Shot is a working app with accounts, payments, email, jobs, security, and mobile shells already built and tested. The complete list of what is included.","content_html":"<p>What is One Shot, and what do you get on day one?</p>\n\n<p>One Shot is a complete, working application that you build your product on top of. Accounts and\npasswordless sign-in, subscriptions and payments, transactional email, background jobs, permissions,\nsecurity hardening, and iPhone and Android app shells are all built, tested, and documented before\nyou write a line. It runs on a fresh clone with <strong>no external accounts and no API keys</strong>, and you own\nthe source. A license is $299 per year at the regular price and includes updates while it is active.</p>\n\n<p>Below is the complete list, including the parts that are deliberately not included.</p>\n\n<h2 id=\"the-short-answer-to-what-is-it-for\">The short answer to \"what is it for\"</h2>\n\n<p>You have an idea. The distance between that idea and something a customer can pay for is mostly work\nthat is identical in every product. One Shot is that work, already done, so you or your coding agent\ncan start at the part that is actually yours.</p>\n\n<h2 id=\"what-is-included\">What is included</h2>\n\n<h3 id=\"accounts-and-sign-in\">Accounts and sign-in</h3>\n\n<ul>\n<li><strong>Passwordless sign-in by email.</strong> A six-digit code, expiring, rate limited, stored as a hash.</li>\n<li><strong>Sign in with Google</strong> and <strong>Sign in with Apple</strong>.</li>\n<li><strong>One signup path.</strong> Every route in creates or finds an account through a single chokepoint, so\nsigning in with Google using an address that already registered by email finds the existing account\nrather than silently creating a duplicate. This is a small detail that causes a specific, painful\nclass of bug when it is missing.</li>\n<li><strong>Sessions</strong> with secure cookies and server-side invalidation on sign out.</li>\n</ul>\n\n<h3 id=\"payments-and-subscriptions\">Payments and subscriptions</h3>\n\n<ul>\n<li><strong>Stripe subscriptions</strong>, with hosted checkout and the customer billing portal.</li>\n<li><strong>A hardened webhook</strong> that verifies signatures and tolerates duplicate delivery, because payment\nproviders retry by design and deliver the same event more than once.</li>\n<li><strong>An entitlement gate.</strong> Put one line in a controller and that feature requires a paid\nsubscription.</li>\n<li><strong>A local stand-in.</strong> With no Stripe key present, the entire billing flow runs in process, end to\nend, on your laptop. Same code path, different implementation behind the seam.</li>\n</ul>\n\n<h3 id=\"email\">Email</h3>\n\n<ul>\n<li><strong>Transactional email</strong> through Resend in production and a local preview window in development, so\nnothing leaves your machine while you build.</li>\n<li><strong>HTML and plain text</strong> versions of every message, which matters for deliverability.</li>\n<li><strong>Async delivery</strong>, so a slow mail provider cannot slow down a web request.</li>\n<li><strong>The DNS records documented</strong>, because <a href=\"/blog/email-that-arrives\" rel=\"noopener\">deliverability is mostly not\ncode</a>.</li>\n</ul>\n\n<h3 id=\"keeping-customers-separate\">Keeping customers separate</h3>\n\n<ul>\n<li><strong>An account is the boundary.</strong> Every piece of customer data belongs to one.</li>\n<li><strong>Permissions</strong> that deny by default, with a scope that filters to the current account\nautomatically.</li>\n<li><strong>A test for it</strong>, shipped with the example feature: a request for another account's record returns\na 404. Copy that test with every resource you add. Details in <a href=\"/blog/keeping-customer-data-separate\" rel=\"noopener\">keeping every customer's data\nseparate</a>.</li>\n</ul>\n\n<h3 id=\"background-jobs\">Background jobs</h3>\n\n<ul>\n<li><strong>Solid Queue</strong>, which runs on your existing database. <strong>No Redis</strong>, no extra service to run, pay\nfor, or monitor. The reasoning is in <a href=\"/blog/background-work-without-extra-services\" rel=\"noopener\">do you need Redis for background\njobs?</a></li>\n<li>Scheduled and recurring jobs included.</li>\n</ul>\n\n<h3 id=\"security-and-compliance\">Security and compliance</h3>\n\n<ul>\n<li>Rate limiting on the endpoints that get abused.</li>\n<li>An <strong>enforced</strong> content security policy, not a commented-out one.</li>\n<li>GDPR data export and account deletion.</li>\n<li>A single command, <code>bin/check</code>, that runs the tests, the linter, a static security scanner, a\ndependency vulnerability audit, and a secret scanner. Measured against <a href=\"/blog/safe-to-launch-checklist\" rel=\"noopener\">the checklist that makes an\napp safe to launch</a>.</li>\n</ul>\n\n<h3 id=\"mobile\">Mobile</h3>\n\n<ul>\n<li><strong>iPhone and Android app shells</strong> that wrap the same web screens, so you maintain one codebase.</li>\n<li>In-app purchase wiring, because both stores require it for digital goods.</li>\n</ul>\n\n<h3 id=\"deployment\">Deployment</h3>\n\n<ul>\n<li>One command to deploy to a server you control, with TLS.</li>\n<li>Runs on <strong>SQLite</strong>, which for most products at most stages is a feature and not a compromise. No\nmanaged database bill on day one.</li>\n</ul>\n\n<h3 id=\"for-your-coding-agent\">For your coding agent</h3>\n\n<ul>\n<li><strong>A guide file</strong> (<code>CLAUDE.md</code> and <code>AGENTS.md</code>) that tells an agent the one right way to do each\nthing in this codebase.</li>\n<li><strong>A worked example feature</strong>, complete with model, permissions, screens, and tests, to copy.</li>\n<li><strong>Skills</strong> for the common operations: add a resource, add an integration, add an email, add a job,\nwrite a post.</li>\n<li><strong>A blog and the whole search and AI discoverability layer</strong>, which is what you are reading. How it\nworks is in <a href=\"/blog/start-a-blog-and-get-found\" rel=\"noopener\">starting a blog and actually getting found</a>.</li>\n</ul>\n\n<h2 id=\"what-is-deliberately-not-included\">What is deliberately not included</h2>\n\n<p>This is the part most descriptions leave out, and it is the part you need in order to decide.</p>\n\n<ul>\n<li><strong>No admin interface.</strong> Data operations go through the console. Building a general-purpose admin UI\nthat nobody's product actually wants is a large amount of code that would be in your way.</li>\n<li><strong>No page builder, no theme system, no plugin architecture.</strong> You edit the code. That is the point.</li>\n<li><strong>No opinion about your product.</strong> There is a worked example feature to copy, and it is meant to be\ndeleted.</li>\n<li><strong>No teams or multi-user workspaces out of the box.</strong> The account boundary is built to support\nthem, and the path is documented, but the shipped model is one user per account.</li>\n<li><strong>No AI features.</strong> There is a documented pattern for adding an integration, and that is the right\nplace to start.</li>\n</ul>\n\n<h2 id=\"the-thing-to-check-before-you-buy\">The thing to check before you buy</h2>\n\n<p>Under the hood it is a Ruby on Rails application. That is not a marketing detail, it is the most\nimportant compatibility question, and it cuts both ways.</p>\n\n<p>If you or your team can read and maintain that, you are getting a conventional, well-tested codebase\nthat you own completely. If nobody on your team can, then a starter kit will not save you six weeks;\nit will hand you a permanent dependency on a stack you do not speak. That is worth knowing before you\nsubscribe, not after.</p>\n\n<p>Everything is written to be conventional and readable on purpose. No metaprogramming, no custom\ndomain-specific languages, no clever abstractions. Boring code is easier for a person to change and\nmuch easier for a coding agent to extend correctly.</p>\n\n<h2 id=\"what-it-costs\">What it costs</h2>\n\n<p>The regular price is $299 per year, and <a href=\"/pricing\" rel=\"noopener\">the pricing page</a> always shows what is actually\nbeing charged today. That includes the full source, delivered privately, and updates for as long as\nthe license is active. If you stop renewing, you keep the code you have.</p>\n\n<p>There is a fuller breakdown of how that compares to the alternatives in <a href=\"/blog/what-it-costs-to-start\" rel=\"noopener\">what it actually costs to\nstart an app</a>.</p>\n\n<h2 id=\"how-you-start\">How you start</h2>\n\n<p>Clone it, run the setup command, and it is already running with a working sign-in and a working\npurchase flow, using local stand-ins for every external service. Then run the bootstrap step, which\nrenames the application, sets your brand, and removes the example feature. Then build your product.</p>\n\n<p>The first thing most people do is point a coding agent at it. There are walkthroughs for <a href=\"/claude\" rel=\"noopener\">Claude\nCode</a>, <a href=\"/codex\" rel=\"noopener\">Codex</a>, <a href=\"/cursor\" rel=\"noopener\">Cursor</a>, <a href=\"/hermes\" rel=\"noopener\">Hermes</a>, and <a href=\"/openclaw\" rel=\"noopener\">OpenClaw</a>.</p>\n","date_published":"2026-07-18T00:00:00+00:00","date_modified":"2026-07-18T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["basics","getting started"]},{"id":"https://shiponeshot.com/blog/where-the-first-run-goes","url":"https://shiponeshot.com/blog/where-the-first-run-goes","title":"Why does your first agent run disappear into plumbing?","summary":"Ask a coding agent for an app and most of its first run goes to sign-in, payments, and email, not your idea. Why that happens, and what to do about it.","content_html":"<p>Why does your first agent run disappear into plumbing?</p>\n\n<p>Because \"build me an app where people can share reading lists\" contains one sentence about reading\nlists and an unwritten twenty about everything that has to exist before a reading list can have an\nowner. Your agent reads the whole request, including the unwritten part, and starts at the bottom.\nBy the time it reaches your actual idea it has spent most of its run, most of its context window, and\nmost of your patience on work that is identical in every product.</p>\n\n<p>Nothing has gone wrong. That is what you asked for.</p>\n\n<h2 id=\"unpacking-one-sentence\">Unpacking one sentence</h2>\n\n<p>Take that request literally and see what it implies.</p>\n\n<p>For someone to have a reading list, there has to be a <em>someone</em>, which means accounts. For accounts\nto be useful across visits, there has to be sign-in, which means sessions, and email verification,\nand a decision about passwords or codes or social sign-in. For a list to be private, every query has\nto be scoped to its owner. For any of it to be online, there has to be a server, a database, a\ncontainer, and a deploy.</p>\n\n<p>And that is the free version. Charge for it and you add a checkout, a webhook, a subscription record,\nand the logic that decides whether this account may currently use the feature.</p>\n\n<p>Somewhere in there is the part you actually care about: what a reading list is, what makes yours\ndifferent, why anyone would use it. It is perhaps five percent of the code and effectively all of the\nproduct.</p>\n\n<h2 id=\"why-the-agent-starts-at-the-bottom\">Why the agent starts at the bottom</h2>\n\n<p>Two reasons, and both are good reasons.</p>\n\n<p><strong>Dependency order.</strong> You cannot write a reading list that belongs to a user before the user exists.\nThe agent is not procrastinating, it is topologically sorting. The plumbing genuinely does come\nfirst.</p>\n\n<p><strong>Ambiguity.</strong> Your one sentence about reading lists is under-specified, and your agent knows it.\nSign-in is not under-specified at all; there is a well-known right answer it has seen thousands of\ntimes. So it does the confident work first and arrives at the ambiguous work later, with less room\nleft, which is precisely backwards from what you want.</p>\n\n<h2 id=\"the-cost-is-not-the-tokens\">The cost is not the tokens</h2>\n\n<p>It is tempting to think of this as a waste of money. It is worse than that, in three specific ways.</p>\n\n<p><strong>The interesting decisions happen last, when there is least room.</strong> Your feature is the part that\nneeds your judgment, several rounds of it. It gets whatever is left of the run and the context\nwindow after the plumbing has consumed both.</p>\n\n<p><strong>You cannot review what you got.</strong> By the end you have a large diff spanning sign-in, payments,\nemail, and your feature. The question you need to answer is not \"does this work\", it is \"is this\nright in the four places where wrong and right look identical\". That is a slow, careful read of code\nyou did not write and did not want to think about. Most people skim it, and the <a href=\"/blog/what-your-agent-should-not-build\" rel=\"noopener\">failures that skim\nmisses</a> surface months later.</p>\n\n<p><strong>There is nothing to check the work against.</strong> In a codebase with no test suite the agent has no way\nto know whether it has finished. It cannot run anything. It reports success because the code it wrote\nlooks like code that works, which is the only signal available to it.</p>\n\n<h2 id=\"what-actually-changes-the-outcome\">What actually changes the outcome</h2>\n\n<p>Not a better prompt. The problem is not phrasing, it is the starting position.</p>\n\n<p>Point the agent at a codebase where accounts, sign-in, payments, email, jobs, permissions, and deploy\nalready exist, are tested, and are documented, and every constraint above inverts:</p>\n\n<ul>\n<li><strong>The dependency order is already satisfied.</strong> Users exist. Accounts exist. The agent starts at\nyour feature because there is nothing underneath it left to build.</li>\n<li><strong>The whole run goes to the ambiguous part</strong>, which is where you actually want its attention and\nyours.</li>\n<li><strong>The diff is small and entirely about your product.</strong> You can review it properly, because it is\nthe part you have opinions about.</li>\n<li><strong>The agent can check itself.</strong> There is a test suite, so \"done\" becomes something it can verify\nrather than assert. It runs the tests, sees what failed, and fixes it before you ever look.</li>\n</ul>\n\n<p>That last one is the biggest change, and it is the one people underestimate. An agent that can verify\nits own work behaves differently from one that cannot. It stops guessing.</p>\n\n<h2 id=\"the-conventions-matter-as-much-as-the-code\">The conventions matter as much as the code</h2>\n\n<p>A subtle point that is easy to miss: a kit gives an agent more than working plumbing. It gives it a\ngrain to follow.</p>\n\n<p>When every existing feature is built the same way, an agent adding the next one has a template.\nIt does not have to invent a structure, and it does not have to guess whether you would prefer this\nor that, because there is a worked example sitting right there and a written note saying \"copy this\nshape\".</p>\n\n<p>That is why the guide file matters so much. A short document that says \"here is the one right way to\ndo each thing, here is the file to copy, here is the rule that must never be broken\" removes most of\nthe decisions an agent would otherwise make on your behalf, badly, at three in the morning. It is\nworth writing carefully, and it is covered in <a href=\"/blog/agent-guide-file\" rel=\"noopener\">how to write a guide file your agent will actually\nfollow</a>.</p>\n\n<h2 id=\"what-good-looks-like\">What good looks like</h2>\n\n<p>Here is the shape of a run that is going well. You ask for a feature. The agent reads the guide file,\nfinds the existing example of a similar feature, copies its structure, writes the model, the\npermission rules, the screens, and the tests, runs the whole suite, fixes the two things that failed,\nand comes back with a diff that is entirely about your feature and nothing else.</p>\n\n<p>That is not a better agent. It is the same agent, standing somewhere else when it starts.</p>\n\n<h2 id=\"the-one-line-version\">The one-line version</h2>\n\n<p>Your first run does not disappear because your agent is weak. It disappears because you asked it to\nbuild a foundation, and foundations are large, mandatory, and identical everywhere.</p>\n\n<p>Give it the foundation and ask for the building.</p>\n\n<p>If you want the concrete list of what that means, see <a href=\"/blog/what-a-starter-kit-is\" rel=\"noopener\">what a starter kit\nis</a> and <a href=\"/blog/whats-in-the-box\" rel=\"noopener\">everything One Shot gives you on day\none</a>.</p>\n","date_published":"2026-07-17T00:00:00+00:00","date_modified":"2026-07-17T00:00:00+00:00","authors":[{"name":"wbnns"}],"tags":["agents","getting started"]}]}