[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-approval-workflows-as-state-machines":3,"blog-nav-approval-workflows-as-state-machines":27},{"slug":4,"title":5,"description":6,"body":7,"category":8,"readTime":9,"tags":10,"coverImage":14,"coverImageAlt":15,"heroImage":16,"heroImageAlt":15,"metaTitle":17,"metaDescription":18,"ogImage":19,"noindex":20,"published":21,"publishAt":22,"sortOrder":23,"updatedAt":22,"status":24,"path":25,"bodyHtml":26},"approval-workflows-as-state-machines","Approval workflows are state machines, not email threads","Draft, submitted, approved, rejected, and expired are the states. Only a few moves are legal, and reject does not mean delete.","A quotation approved in chat has no state. It has a thread. A week later nobody can say whether it was approved, replaced, or answered with “ok.”\n\nThe states are the domain. [Model that before the controllers](\u002Fblog\u002Fwhy-we-model-the-domain-before-writing-controllers). [Scoping a custom system](\u002Fblog\u002Fscoping-a-custom-system) is where you name this boundary, before a feature list grows a status dropdown with every word someone might type. [Internal tools and customer-facing apps](\u002Fblog\u002Finternal-tools-vs-customer-facing-apps) are scoped differently. An approval flow is an internal tool. It should not be designed like a marketing page.\n\n## The states we mean\n\n| State | What is true |\n|-------|----------------|\n| Draft | The author can still change it |\n| Submitted | A named approver has it. The author does not silently edit the amount |\n| Approved | That number stands. A change is a new revision, not an overwrite |\n| Rejected | Kept, with a reason. Not deleted |\n| Expired | The date passed. Payment does not make it approved |\n\nAllowed moves are few. Draft goes to submitted. Submitted goes to approved or rejected. Approved does not return to draft. A rejected document can be copied into a new draft. It is not edited until it looks approved.\n\nRecord who moved it and when. That row is the audit. Email is a notification, and the notification belongs on a [queue](\u002Fblog\u002Fqueues-work-outside-the-http-request), not in the transaction that wrote the state.\n\n## What the screen must not do\n\n- Offer every state to every role. The receptionist does not get “approved.”\n- Treat delete as a synonym for reject.\n- Leave two documents marked current for one request.\n\nIf the approver is “whoever sees the chat,” you do not have a workflow yet. Name the role, then build the transition.\n\n## Straight next step\n\n[Send one document that is approved in chat today](\u002Fcontact) — a quote, a discount, a purchase. We will name the states and who may move them. The screens come after that, on a [custom system](\u002Fservices\u002Fcustom-systems-web-applications) if the thread is actually how the business decides.","Engineering",6,[11,12,13],"Workflow","Domain","Laravel","\u002Fimages\u002Fjournal\u002Fapproval-workflows-as-state-machines-cover.webp?v=20261013b","Printed form on a clipboard beside a laptop","\u002Fimages\u002Fjournal\u002Fapproval-workflows-as-state-machines-hero.webp?v=20261013b","Approval Workflows as State Machines | PrimeCode","How a quotation or purchase approval should move through explicit states, with one audit trail and no silent overwrites.","\u002Fimages\u002Fjournal\u002Fapproval-workflows-as-state-machines-og.webp?v=20261013b",false,true,"2026-10-28T01:00:00.000Z",57,"live","\u002Fblog\u002Fapproval-workflows-as-state-machines","\u003Cp>A quotation approved in chat has no state. It has a thread. A week later nobody can say whether it was approved, replaced, or answered with “ok.”\u003C\u002Fp>\n\u003Cp>The states are the domain. \u003Ca href=\"\u002Fblog\u002Fwhy-we-model-the-domain-before-writing-controllers\" rel=\"noopener noreferrer\">Model that before the controllers\u003C\u002Fa>. \u003Ca href=\"\u002Fblog\u002Fscoping-a-custom-system\" rel=\"noopener noreferrer\">Scoping a custom system\u003C\u002Fa> is where you name this boundary, before a feature list grows a status dropdown with every word someone might type. \u003Ca href=\"\u002Fblog\u002Finternal-tools-vs-customer-facing-apps\" rel=\"noopener noreferrer\">Internal tools and customer-facing apps\u003C\u002Fa> are scoped differently. An approval flow is an internal tool. It should not be designed like a marketing page.\u003C\u002Fp>\n\u003Ch2>The states we mean\u003C\u002Fh2>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>State\u003C\u002Fth>\n\u003Cth>What is true\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>Draft\u003C\u002Ftd>\n\u003Ctd>The author can still change it\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Submitted\u003C\u002Ftd>\n\u003Ctd>A named approver has it. The author does not silently edit the amount\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Approved\u003C\u002Ftd>\n\u003Ctd>That number stands. A change is a new revision, not an overwrite\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Rejected\u003C\u002Ftd>\n\u003Ctd>Kept, with a reason. Not deleted\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Expired\u003C\u002Ftd>\n\u003Ctd>The date passed. Payment does not make it approved\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Allowed moves are few. Draft goes to submitted. Submitted goes to approved or rejected. Approved does not return to draft. A rejected document can be copied into a new draft. It is not edited until it looks approved.\u003C\u002Fp>\n\u003Cp>Record who moved it and when. That row is the audit. Email is a notification, and the notification belongs on a \u003Ca href=\"\u002Fblog\u002Fqueues-work-outside-the-http-request\" rel=\"noopener noreferrer\">queue\u003C\u002Fa>, not in the transaction that wrote the state.\u003C\u002Fp>\n\u003Ch2>What the screen must not do\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>Offer every state to every role. The receptionist does not get “approved.”\u003C\u002Fli>\n\u003Cli>Treat delete as a synonym for reject.\u003C\u002Fli>\n\u003Cli>Leave two documents marked current for one request.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>If the approver is “whoever sees the chat,” you do not have a workflow yet. Name the role, then build the transition.\u003C\u002Fp>\n\u003Ch2>Straight next step\u003C\u002Fh2>\n\u003Cp>\u003Ca href=\"\u002Fcontact\" rel=\"noopener noreferrer\">Send one document that is approved in chat today\u003C\u002Fa> — a quote, a discount, a purchase. We will name the states and who may move them. The screens come after that, on a \u003Ca href=\"\u002Fservices\u002Fcustom-systems-web-applications\" rel=\"noopener noreferrer\">custom system\u003C\u002Fa> if the thread is actually how the business decides.\u003C\u002Fp>\n",{"prev":28,"related":46},{"slug":29,"title":30,"description":31,"category":8,"readTime":32,"tags":33,"coverImage":37,"coverImageAlt":38,"heroImage":39,"heroImageAlt":38,"metaTitle":40,"metaDescription":41,"ogImage":42,"noindex":20,"published":21,"publishAt":43,"sortOrder":44,"updatedAt":43,"status":24,"path":45},"one-database-many-branches-row-scope","One database, many branches: scope rows, do not split the system","A second site is a branch column, not a second app. Stock and schedules stay scoped. Queries, exports, and jobs must all carry that scope.",7,[34,35,36],"Architecture","Database","Branches","\u002Fimages\u002Fjournal\u002Fone-database-many-branches-row-scope-cover.webp?v=20261013b","Open sign hanging in a shop window","\u002Fimages\u002Fjournal\u002Fone-database-many-branches-row-scope-hero.webp?v=20261013b","One Database, Many Branches | PrimeCode","How a business system scopes stock and schedules to a branch without cloning the database or standing up microservices.","\u002Fimages\u002Fjournal\u002Fone-database-many-branches-row-scope-og.webp?v=20261013b","2026-10-26T01:00:00.000Z",56,"\u002Fblog\u002Fone-database-many-branches-row-scope",[47,49,65],{"slug":29,"title":30,"description":31,"category":8,"readTime":32,"tags":48,"coverImage":37,"coverImageAlt":38,"heroImage":39,"heroImageAlt":38,"metaTitle":40,"metaDescription":41,"ogImage":42,"noindex":20,"published":21,"publishAt":43,"sortOrder":44,"updatedAt":43,"status":24,"path":45},[34,35,36],{"slug":50,"title":51,"description":52,"category":8,"readTime":9,"tags":53,"coverImage":56,"coverImageAlt":57,"heroImage":58,"heroImageAlt":57,"metaTitle":59,"metaDescription":60,"ogImage":61,"noindex":20,"published":21,"publishAt":62,"sortOrder":63,"updatedAt":62,"status":24,"path":64},"sanctum-cookie-sessions-vs-bearer-tokens","Cookie sessions or bearer tokens for a Nuxt and Laravel app","A first-party Nuxt app should use a Sanctum cookie session. Bearer tokens belong to mobile apps and other clients, and they must be revocable.",[54,13,55],"Auth","Nuxt","\u002Fimages\u002Fjournal\u002Fsanctum-cookie-sessions-vs-bearer-tokens-cover.webp?v=20261013b","Person at a laptop in an office","\u002Fimages\u002Fjournal\u002Fsanctum-cookie-sessions-vs-bearer-tokens-hero.webp?v=20261013b","Cookie Sessions vs Bearer Tokens | PrimeCode","When a Nuxt and Laravel app should use Sanctum cookie sessions, and when a bearer token is the right client.","\u002Fimages\u002Fjournal\u002Fsanctum-cookie-sessions-vs-bearer-tokens-og.webp?v=20261013b","2026-10-23T01:00:00.000Z",55,"\u002Fblog\u002Fsanctum-cookie-sessions-vs-bearer-tokens",{"slug":66,"title":67,"description":68,"category":8,"readTime":32,"tags":69,"coverImage":72,"coverImageAlt":73,"heroImage":74,"heroImageAlt":73,"metaTitle":75,"metaDescription":76,"ogImage":77,"noindex":20,"published":21,"publishAt":78,"sortOrder":79,"updatedAt":78,"status":24,"path":80},"prevent-double-booking-reservation-concurrency","How a reservation system prevents double booking","Two requests can both read a free slot. A unique or exclusion constraint, plus a hold that expires, is what rejects the second insert.",[70,35,71],"Bookings","Concurrency","\u002Fimages\u002Fjournal\u002Fprevent-double-booking-reservation-concurrency-cover.webp?v=20261013b","Open calendar planner with a coffee cup","\u002Fimages\u002Fjournal\u002Fprevent-double-booking-reservation-concurrency-hero.webp?v=20261013b","Preventing Double Bookings in a Reservation System | PrimeCode","Why two simultaneous bookings both see a free slot, and how a constraint and a short hold stop the duplicate.","\u002Fimages\u002Fjournal\u002Fprevent-double-booking-reservation-concurrency-og.webp?v=20261013b","2026-10-21T01:00:00.000Z",54,"\u002Fblog\u002Fprevent-double-booking-reservation-concurrency"]