[{"data":1,"prerenderedAt":1573},["ShallowReactive",2],{"navigation_docs_en":3,"-en-getting-started-coming-from-laravel":342,"-en-getting-started-coming-from-laravel-surround":1568},[4,45,61,96,132,166,196,217,241,265],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":44},"Getting Started","i-lucide-rocket","\u002Fen\u002Fgetting-started","en\u002F1.getting-started",[10,15,20,24,29,34,39],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fgetting-started\u002Fintroduction","en\u002F1.getting-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Coming from Laravel","\u002Fen\u002Fgetting-started\u002Fcoming-from-laravel","en\u002F1.getting-started\u002F2.coming-from-laravel","i-lucide-arrow-right-left",{"title":21,"path":22,"stem":23,"icon":19},"Coming from Symfony","\u002Fen\u002Fgetting-started\u002Fcoming-from-symfony","en\u002F1.getting-started\u002F3.coming-from-symfony",{"title":25,"path":26,"stem":27,"icon":28},"Installation","\u002Fen\u002Fgetting-started\u002Finstallation","en\u002F1.getting-started\u002F4.installation","i-lucide-download",{"title":30,"path":31,"stem":32,"icon":33},"Your first module","\u002Fen\u002Fgetting-started\u002Ffirst-module","en\u002F1.getting-started\u002F5.first-module","i-lucide-package-plus",{"title":35,"path":36,"stem":37,"icon":38},"Project structure","\u002Fen\u002Fgetting-started\u002Fproject-structure","en\u002F1.getting-started\u002F6.project-structure","i-lucide-folder-tree",{"title":40,"path":41,"stem":42,"icon":43},"The shop sample application","\u002Fen\u002Fgetting-started\u002Fsample-app","en\u002F1.getting-started\u002F7.sample-app","i-lucide-shopping-cart",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"Reference","i-lucide-book-open","\u002Fen\u002Freference","en\u002F10.reference",[51,56],{"title":52,"path":53,"stem":54,"icon":55},"Configuration","\u002Fen\u002Freference\u002Fconfiguration","en\u002F10.reference\u002F1.configuration","i-lucide-settings",{"title":57,"path":58,"stem":59,"icon":60},"External dependencies","\u002Fen\u002Freference\u002Fdependencies","en\u002F10.reference\u002F2.dependencies","i-lucide-package",{"title":62,"icon":63,"path":64,"stem":65,"children":66,"page":44},"Concepts","i-lucide-lightbulb","\u002Fen\u002Fconcepts","en\u002F2.concepts",[67,72,77,82,86,91],{"title":68,"path":69,"stem":70,"icon":71},"Architecture","\u002Fen\u002Fconcepts\u002Farchitecture","en\u002F2.concepts\u002F1.architecture","i-lucide-layers",{"title":73,"path":74,"stem":75,"icon":76},"Application lifecycle","\u002Fen\u002Fconcepts\u002Fapplication-lifecycle","en\u002F2.concepts\u002F2.application-lifecycle","i-lucide-power",{"title":78,"path":79,"stem":80,"icon":81},"Error model","\u002Fen\u002Fconcepts\u002Ferror-model","en\u002F2.concepts\u002F3.error-model","i-lucide-shield-alert",{"title":52,"path":83,"stem":84,"icon":85},"\u002Fen\u002Fconcepts\u002Fconfiguration","en\u002F2.concepts\u002F4.configuration","i-lucide-settings-2",{"title":87,"path":88,"stem":89,"icon":90},"Codegen pipeline","\u002Fen\u002Fconcepts\u002Fcodegen-pipeline","en\u002F2.concepts\u002F5.codegen-pipeline","i-lucide-file-json",{"title":92,"path":93,"stem":94,"icon":95},"Design patterns","\u002Fen\u002Fconcepts\u002Fdesign-patterns","en\u002F2.concepts\u002F6.design-patterns","i-lucide-puzzle",{"title":97,"icon":98,"path":99,"stem":100,"children":101,"page":44},"HTTP & Routing","i-lucide-globe","\u002Fen\u002Fhttp","en\u002F3.http",[102,107,112,117,122,127],{"title":103,"path":104,"stem":105,"icon":106},"The server core","\u002Fen\u002Fhttp\u002Fserver","en\u002F3.http\u002F1.server","i-lucide-server",{"title":108,"path":109,"stem":110,"icon":111},"The Fiber engine","\u002Fen\u002Fhttp\u002Ffiber","en\u002F3.http\u002F2.fiber","i-lucide-zap",{"title":113,"path":114,"stem":115,"icon":116},"The chi engine","\u002Fen\u002Fhttp\u002Fchi","en\u002F3.http\u002F3.chi","i-lucide-route",{"title":118,"path":119,"stem":120,"icon":121},"Error responses","\u002Fen\u002Fhttp\u002Ferror-responses","en\u002F3.http\u002F4.error-responses","i-lucide-octagon-alert",{"title":123,"path":124,"stem":125,"icon":126},"Validation","\u002Fen\u002Fhttp\u002Fvalidation","en\u002F3.http\u002F5.validation","i-lucide-badge-check",{"title":128,"path":129,"stem":130,"icon":131},"Pagination","\u002Fen\u002Fhttp\u002Fpagination","en\u002F3.http\u002F6.pagination","i-lucide-list-ordered",{"title":133,"icon":134,"path":135,"stem":136,"children":137,"page":44},"Database","i-lucide-database","\u002Fen\u002Fdatabase","en\u002F4.database",[138,142,147,151,156,161],{"title":139,"path":140,"stem":141,"icon":134},"Overview","\u002Fen\u002Fdatabase\u002Foverview","en\u002F4.database\u002F1.overview",{"title":143,"path":144,"stem":145,"icon":146},"pgx","\u002Fen\u002Fdatabase\u002Fpgx","en\u002F4.database\u002F2.pgx","i-lucide-plug",{"title":148,"path":149,"stem":150,"icon":71},"bun","\u002Fen\u002Fdatabase\u002Fbun","en\u002F4.database\u002F3.bun",{"title":152,"path":153,"stem":154,"icon":155},"Transactions","\u002Fen\u002Fdatabase\u002Ftransactions","en\u002F4.database\u002F4.transactions","i-lucide-git-merge",{"title":157,"path":158,"stem":159,"icon":160},"Migrations","\u002Fen\u002Fdatabase\u002Fmigrations","en\u002F4.database\u002F5.migrations","i-lucide-file-stack",{"title":162,"path":163,"stem":164,"icon":165},"Seeders","\u002Fen\u002Fdatabase\u002Fseeding","en\u002F4.database\u002F6.seeding","i-lucide-sprout",{"title":167,"icon":168,"path":169,"stem":170,"children":171,"page":44},"Async & Background","i-lucide-workflow","\u002Fen\u002Fasync","en\u002F5.async",[172,176,181,186,191],{"title":173,"path":174,"stem":175,"icon":131},"Queue","\u002Fen\u002Fasync\u002Fqueue","en\u002F5.async\u002F1.queue",{"title":177,"path":178,"stem":179,"icon":180},"Events","\u002Fen\u002Fasync\u002Fevents","en\u002F5.async\u002F2.events","i-lucide-radio",{"title":182,"path":183,"stem":184,"icon":185},"Outbox","\u002Fen\u002Fasync\u002Foutbox","en\u002F5.async\u002F3.outbox","i-lucide-inbox",{"title":187,"path":188,"stem":189,"icon":190},"Scheduling","\u002Fen\u002Fasync\u002Fscheduler","en\u002F5.async\u002F4.scheduler","i-lucide-calendar-clock",{"title":192,"path":193,"stem":194,"icon":195},"Worker","\u002Fen\u002Fasync\u002Fworker","en\u002F5.async\u002F5.worker","i-lucide-cog",{"title":197,"icon":198,"path":199,"stem":200,"children":201,"page":44},"Security","i-lucide-shield-check","\u002Fen\u002Fsecurity","en\u002F6.security",[202,207,212],{"title":203,"path":204,"stem":205,"icon":206},"Authentication","\u002Fen\u002Fsecurity\u002Fauthentication","en\u002F6.security\u002F1.authentication","i-lucide-key-round",{"title":208,"path":209,"stem":210,"icon":211},"RBAC","\u002Fen\u002Fsecurity\u002Frbac","en\u002F6.security\u002F2.rbac","i-lucide-users",{"title":213,"path":214,"stem":215,"icon":216},"Policies","\u002Fen\u002Fsecurity\u002Fpolicies","en\u002F6.security\u002F3.policies","i-lucide-gavel",{"title":218,"icon":219,"path":220,"stem":221,"children":222,"page":44},"Observability","i-lucide-activity","\u002Fen\u002Fobservability","en\u002F7.observability",[223,226,231,236],{"title":139,"path":224,"stem":225,"icon":219},"\u002Fen\u002Fobservability\u002Foverview","en\u002F7.observability\u002F1.overview",{"title":227,"path":228,"stem":229,"icon":230},"Logging","\u002Fen\u002Fobservability\u002Flogging","en\u002F7.observability\u002F2.logging","i-lucide-scroll-text",{"title":232,"path":233,"stem":234,"icon":235},"OpenTelemetry","\u002Fen\u002Fobservability\u002Fopentelemetry","en\u002F7.observability\u002F3.opentelemetry","i-lucide-radar",{"title":237,"path":238,"stem":239,"icon":240},"Sentry","\u002Fen\u002Fobservability\u002Fsentry","en\u002F7.observability\u002F4.sentry","i-lucide-bug",{"title":242,"icon":243,"path":244,"stem":245,"children":246,"page":44},"Storage & Mail","i-lucide-hard-drive","\u002Fen\u002Fstorage-mail","en\u002F8.storage-mail",[247,251,256,261],{"title":248,"path":249,"stem":250,"icon":243},"Storage","\u002Fen\u002Fstorage-mail\u002Fstorage","en\u002F8.storage-mail\u002F1.storage",{"title":252,"path":253,"stem":254,"icon":255},"Email","\u002Fen\u002Fstorage-mail\u002Fmail","en\u002F8.storage-mail\u002F2.mail","i-lucide-mail",{"title":257,"path":258,"stem":259,"icon":260},"Notifications","\u002Fen\u002Fstorage-mail\u002Fnotifications","en\u002F8.storage-mail\u002F3.notifications","i-lucide-bell",{"title":262,"path":263,"stem":264,"icon":180},"Realtime","\u002Fen\u002Fstorage-mail\u002Frealtime","en\u002F8.storage-mail\u002F4.realtime",{"title":266,"icon":267,"path":268,"stem":269,"children":270,"page":44},"CLI Reference","i-lucide-terminal","\u002Fen\u002Fcli","en\u002F9.cli",[271,274,279,284,289,294,299,303,308,313,318,323,328,333,337],{"title":139,"path":272,"stem":273,"icon":267},"\u002Fen\u002Fcli\u002Foverview","en\u002F9.cli\u002F1.overview",{"title":275,"path":276,"stem":277,"icon":278},"add db","\u002Fen\u002Fcli\u002Fadd-db","en\u002F9.cli\u002F10.add-db","i-lucide-database-zap",{"title":280,"path":281,"stem":282,"icon":283},"add compose \u002F add docker","\u002Fen\u002Fcli\u002Fadd-compose","en\u002F9.cli\u002F11.add-compose","i-lucide-container",{"title":285,"path":286,"stem":287,"icon":288},"add mail","\u002Fen\u002Fcli\u002Fadd-mail","en\u002F9.cli\u002F12.add-mail","i-lucide-mail-plus",{"title":290,"path":291,"stem":292,"icon":293},"add notification","\u002Fen\u002Fcli\u002Fadd-notification","en\u002F9.cli\u002F13.add-notification","i-lucide-bell-plus",{"title":295,"path":296,"stem":297,"icon":298},"add realtime","\u002Fen\u002Fcli\u002Fadd-realtime","en\u002F9.cli\u002F14.add-realtime","i-lucide-radio-tower",{"title":300,"path":301,"stem":302,"icon":165},"add seeder","\u002Fen\u002Fcli\u002Fadd-seeder","en\u002F9.cli\u002F15.add-seeder",{"title":304,"path":305,"stem":306,"icon":307},"new project","\u002Fen\u002Fcli\u002Fnew-project","en\u002F9.cli\u002F2.new-project","i-lucide-folder-plus",{"title":309,"path":310,"stem":311,"icon":312},"new module","\u002Fen\u002Fcli\u002Fnew-module","en\u002F9.cli\u002F3.new-module","i-lucide-blocks",{"title":314,"path":315,"stem":316,"icon":317},"add surface","\u002Fen\u002Fcli\u002Fadd-surface","en\u002F9.cli\u002F4.add-surface","i-lucide-layers-2",{"title":319,"path":320,"stem":321,"icon":322},"add core","\u002Fen\u002Fcli\u002Fadd-core","en\u002F9.cli\u002F5.add-core","i-lucide-box",{"title":324,"path":325,"stem":326,"icon":327},"add handler","\u002Fen\u002Fcli\u002Fadd-handler","en\u002F9.cli\u002F6.add-handler","i-lucide-webhook",{"title":329,"path":330,"stem":331,"icon":332},"new migration","\u002Fen\u002Fcli\u002Fnew-migration","en\u002F9.cli\u002F7.new-migration","i-lucide-file-plus",{"title":334,"path":335,"stem":336,"icon":195},"worker generators","\u002Fen\u002Fcli\u002Fworker-generators","en\u002F9.cli\u002F8.worker-generators",{"title":338,"path":339,"stem":340,"icon":341},"upgrade templates","\u002Fen\u002Fcli\u002Fupgrade-templates","en\u002F9.cli\u002F9.upgrade-templates","i-lucide-refresh-cw",{"id":343,"title":16,"body":344,"description":1561,"extension":1562,"links":1563,"meta":1564,"navigation":1565,"path":17,"seo":1566,"stem":18,"__hash__":1567},"docs_en\u002Fen\u002F1.getting-started\u002F2.coming-from-laravel.md",{"type":345,"value":346,"toc":1554},"minimark",[347,356,368,373,790,794,801,981,984,1375,1389,1393,1399,1490,1494,1525,1529,1550],[348,349,350,351,355],"p",{},"If you've worked with Laravel, most of gpsystem's building blocks will feel familiar: several packages were explicitly modeled on their Laravel counterparts. This page shows what maps to what, and honestly, what does ",[352,353,354],"strong",{},"not"," transfer.",[348,357,358,359,363,364,367],{},"What carries over: the way you think about events, listeners, policies, scheduling and queues transfers one to one. What doesn't: there is no ORM magic (no Eloquent, repositories contain SQL or the bun query builder), no container autowiring (dependencies travel in an explicit ",[360,361,362],"code",{},"Dependencies"," struct that you fill in ",[360,365,366],{},"main.go","), and no global facades either: every dependency is visible and traceable.",[369,370,372],"h2",{"id":371},"the-mapping-table","The mapping table",[374,375,376,392],"table",{},[377,378,379],"thead",{},[380,381,382,386,389],"tr",{},[383,384,385],"th",{},"Laravel",[383,387,388],{},"gpsystem",[383,390,391],{},"Where",[393,394,395,424,446,471,489,510,525,540,557,574,586,607,630,646,666,683,705,717,738,758,774],"tbody",{},[380,396,397,405,418],{},[398,399,400,401,404],"td",{},"Task Scheduling (",[360,402,403],{},"$schedule->daily()",")",[398,406,407,410,411,414,415,404],{},[360,408,409],{},"scheduler.Schedule"," fluent API (",[360,412,413],{},"sched.DailyAt(...)",", ",[360,416,417],{},"sched.Job(...)",[398,419,420],{},[421,422,423],"a",{"href":188},"Scheduler",[380,425,426,432,442],{},[398,427,428,429],{},"Events & Listeners, ",[360,430,431],{},"ShouldQueue",[398,433,434,437,438,441],{},[360,435,436],{},"events.Event"," + ",[360,439,440],{},"events.Listen"," (every listener is an independent, retried task)",[398,443,444],{},[421,445,177],{"href":178},[380,447,448,455,465],{},[398,449,450,451,454],{},"Queues, ",[360,452,453],{},"queue:work",", Horizon",[398,456,457,460,461,464],{},[360,458,459],{},"queue"," (asynq + Redis) + the ",[360,462,463],{},"cmd\u002Fworker"," binary",[398,466,467,414,469],{},[421,468,173],{"href":174},[421,470,192],{"href":193},[380,472,473,479,485],{},[398,474,475,476,404],{},"Policies (",[360,477,478],{},"$this->authorize()",[398,480,481,484],{},[360,482,483],{},"policy.Registry"," + generated enforcer middleware, fail-closed",[398,486,487],{},[421,488,213],{"href":214},[380,490,491,494,506],{},[398,492,493],{},"Gates, roles (spatie\u002Fpermission)",[398,495,496,414,499,502,503],{},[360,497,498],{},"rbac.Identity",[360,500,501],{},"RequireRole"," \u002F ",[360,504,505],{},"RequirePermission",[398,507,508],{},[421,509,208],{"href":209},[380,511,512,515,521],{},[398,513,514],{},"Auth guards, Sanctum tokens",[398,516,517,520],{},[360,518,519],{},"auth"," (JWT issuing and middleware)",[398,522,523],{},[421,524,203],{"href":204},[380,526,527,530,536],{},[398,528,529],{},"Monolog (dev channel)",[398,531,532,535],{},[360,533,534],{},"logx\u002Fmonolog"," (a Monolog-format console handler)",[398,537,538],{},[421,539,227],{"href":228},[380,541,542,547,553],{},[398,543,544],{},[360,545,546],{},"php artisan migrate",[398,548,549,552],{},[360,550,551],{},"go run .\u002Fcmd\u002Fmigrate"," (embedded, numbered SQL)",[398,554,555],{},[421,556,157],{"href":158},[380,558,559,564,570],{},[398,560,561],{},[360,562,563],{},"DB::transaction(fn)",[398,565,566,569],{},[360,567,568],{},"Transactor.WithinTransaction(ctx, fn)"," (the transaction travels in the context)",[398,571,572],{},[421,573,152],{"href":153},[380,575,576,579,582],{},[398,577,578],{},"FormRequest rules",[398,580,581],{},"validator struct tags + generated request types",[398,583,584],{},[421,585,123],{"href":124},[380,587,588,597,603],{},[398,589,590,591,502,594],{},"Exception ",[360,592,593],{},"render()",[360,595,596],{},"report()",[398,598,599,602],{},[360,600,601],{},"errs"," codes + public message → RFC 9457 problem+json \u002F Sentry",[398,604,605],{},[421,606,78],{"href":79},[380,608,609,617,626],{},[398,610,611,437,614],{},[360,612,613],{},"config()",[360,615,616],{},".env",[398,618,619,622,623,625],{},[360,620,621],{},"envconf.MustLoad[T]"," (a typed struct from env, with ",[360,624,616],{}," support)",[398,627,628],{},[421,629,52],{"href":83},[380,631,632,635,642],{},[398,633,634],{},"Storage facade, flysystem disks",[398,636,637,638,641],{},"the ",[360,639,640],{},"storage.Store"," interface + an S3 implementation",[398,643,644],{},[421,645,248],{"href":249},[380,647,648,651,661],{},[398,649,650],{},"Mailables, markdown mail",[398,652,637,653,656,657,660],{},[360,654,655],{},"mail.Message"," builder + MJML templates; ",[360,658,659],{},"Mail::fake()"," ↔ the memory mailer",[398,662,663],{},[421,664,665],{"href":253},"Mail",[380,667,668,673,678],{},[398,669,670],{},[360,671,672],{},"php artisan make:*",[398,674,675],{},[360,676,677],{},"go tool gpsystem new\u002Fadd ...",[398,679,680],{},[421,681,682],{"href":272},"CLI",[380,684,685,693,701],{},[398,686,687,502,690],{},[360,688,689],{},"paginate()",[360,691,692],{},"cursorPaginate()",[398,694,695,502,698],{},[360,696,697],{},"paginate.Page[T]",[360,699,700],{},"paginate.CursorPage[T]",[398,702,703],{},[421,704,128],{"href":129},[380,706,707,710,713],{},[398,708,709],{},"Eloquent ORM",[398,711,712],{},"pgx (SQL) or bun (query builder), no ActiveRecord simulation",[398,714,715],{},[421,716,133],{"href":140},[380,718,719,726,734],{},[398,720,721,722,725],{},"Route registration (",[360,723,724],{},"routes\u002Fapi.php",", auto-discovery)",[398,727,728,731,732,404],{},[360,729,730],{},"Register\u003CSurface>(router, deps)"," (an explicit call in ",[360,733,366],{},[398,735,736],{},[421,737,68],{"href":69},[380,739,740,743,754],{},[398,741,742],{},"A route group \u002F separate controller set per audience (public API vs admin)",[398,744,745,746,749,750,753],{},"a ",[352,747,748],{},"surface"," (per-audience bundle): ",[360,751,752],{},"surfaces\u002F\u003Csurface>\u002F",", with its own contract and middleware",[398,755,756],{},[421,757,314],{"href":315},[380,759,760,763,770],{},[398,761,762],{},"Model methods + shared service logic",[398,764,765,766,769],{},"the module's ",[360,767,768],{},"core\u002F"," package (rules written once, called by every surface)",[398,771,772],{},[421,773,35],{"href":36},[380,775,776,779,786],{},[398,777,778],{},"The query layer (Eloquent scopes, query builder calls)",[398,780,781,782,785],{},"the module-level ",[360,783,784],{},"repository\u002F",": one per module, shared by every surface",[398,787,788],{},[421,789,35],{"href":36},[369,791,793],{"id":792},"two-examples-side-by-side","Two examples side by side",[348,795,796,797,800],{},"A scheduled task: the behavior of ",[360,798,799],{},"->onOneServer()"," is the default here (Redis-lease leader election):",[802,803,804,877],"code-group",{},[805,806,812],"pre",{"className":807,"code":808,"filename":809,"language":810,"meta":811,"style":811},"language-php shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","Schedule::command('report:nightly-sales')\n    ->dailyAt('03:00')\n    ->onOneServer();\n","Laravel: routes\u002Fconsole.php","php","",[360,813,814,846,866],{"__ignoreMap":811},[815,816,819,823,827,831,834,837,841,843],"span",{"class":817,"line":818},"line",1,[815,820,822],{"class":821},"sBMFI","Schedule",[815,824,826],{"class":825},"sMK4o","::",[815,828,830],{"class":829},"s2Zo4","command",[815,832,833],{"class":825},"(",[815,835,836],{"class":825},"'",[815,838,840],{"class":839},"sfazB","report:nightly-sales",[815,842,836],{"class":825},[815,844,845],{"class":825},")\n",[815,847,849,852,855,857,859,862,864],{"class":817,"line":848},2,[815,850,851],{"class":825},"    ->",[815,853,854],{"class":829},"dailyAt",[815,856,833],{"class":825},[815,858,836],{"class":825},[815,860,861],{"class":839},"03:00",[815,863,836],{"class":825},[815,865,845],{"class":825},[815,867,869,871,874],{"class":817,"line":868},3,[815,870,851],{"class":825},[815,872,873],{"class":829},"onOneServer",[815,875,876],{"class":825},"();\n",[805,878,883],{"className":879,"code":880,"filename":881,"language":882,"meta":811,"style":811},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","func Register(sched *scheduler.Schedule, deps shop.Dependencies) {\n    sched.Job(\"shop.nightlySalesReport\", \"0 3 * * *\",\n        nightlySalesReport(deps))\n}\n","gpsystem: internal\u002Fmodules\u002Fshop\u002Fjobs\u002Fregister.go","go",[360,884,885,928,962,975],{"__ignoreMap":811},[815,886,887,890,893,895,899,902,905,908,910,913,916,919,921,923,925],{"class":817,"line":818},[815,888,889],{"class":825},"func",[815,891,892],{"class":829}," Register",[815,894,833],{"class":825},[815,896,898],{"class":897},"sHdIc","sched",[815,900,901],{"class":825}," *",[815,903,904],{"class":821},"scheduler",[815,906,907],{"class":825},".",[815,909,822],{"class":821},[815,911,912],{"class":825},",",[815,914,915],{"class":897}," deps",[815,917,918],{"class":821}," shop",[815,920,907],{"class":825},[815,922,362],{"class":821},[815,924,404],{"class":825},[815,926,927],{"class":825}," {\n",[815,929,930,934,936,939,941,944,947,949,951,954,957,959],{"class":817,"line":848},[815,931,933],{"class":932},"sTEyZ","    sched",[815,935,907],{"class":825},[815,937,938],{"class":829},"Job",[815,940,833],{"class":825},[815,942,943],{"class":825},"\"",[815,945,946],{"class":839},"shop.nightlySalesReport",[815,948,943],{"class":825},[815,950,912],{"class":825},[815,952,953],{"class":825}," \"",[815,955,956],{"class":839},"0 3 * * *",[815,958,943],{"class":825},[815,960,961],{"class":825},",\n",[815,963,964,967,969,972],{"class":817,"line":868},[815,965,966],{"class":829},"        nightlySalesReport",[815,968,833],{"class":825},[815,970,971],{"class":932},"deps",[815,973,974],{"class":825},"))\n",[815,976,978],{"class":817,"line":977},4,[815,979,980],{"class":825},"}\n",[348,982,983],{},"A policy (per-request ownership checks for when a role alone isn't enough):",[802,985,986,1117],{},[805,987,990],{"className":807,"code":988,"filename":989,"language":810,"meta":811,"style":811},"class OrderPolicy\n{\n    public function view(User $user, Order $order): bool\n    {\n        return $user->id === $order->user_id\n            || $user->hasRole('admin');\n    }\n}\n","Laravel: app\u002FPolicies\u002FOrderPolicy.php",[360,991,992,1001,1006,1045,1050,1079,1106,1112],{"__ignoreMap":811},[815,993,994,998],{"class":817,"line":818},[815,995,997],{"class":996},"spNyl","class",[815,999,1000],{"class":821}," OrderPolicy\n",[815,1002,1003],{"class":817,"line":848},[815,1004,1005],{"class":825},"{\n",[815,1007,1008,1011,1014,1017,1019,1022,1025,1028,1030,1033,1035,1038,1041],{"class":817,"line":868},[815,1009,1010],{"class":996},"    public",[815,1012,1013],{"class":996}," function",[815,1015,1016],{"class":829}," view",[815,1018,833],{"class":825},[815,1020,1021],{"class":821},"User",[815,1023,1024],{"class":825}," $",[815,1026,1027],{"class":932},"user",[815,1029,912],{"class":825},[815,1031,1032],{"class":821}," Order",[815,1034,1024],{"class":825},[815,1036,1037],{"class":932},"order",[815,1039,1040],{"class":825},"):",[815,1042,1044],{"class":1043},"sbssI"," bool\n",[815,1046,1047],{"class":817,"line":977},[815,1048,1049],{"class":825},"    {\n",[815,1051,1053,1057,1059,1061,1064,1067,1070,1072,1074,1076],{"class":817,"line":1052},5,[815,1054,1056],{"class":1055},"s7zQu","        return",[815,1058,1024],{"class":825},[815,1060,1027],{"class":932},[815,1062,1063],{"class":825},"->",[815,1065,1066],{"class":932},"id ",[815,1068,1069],{"class":825},"===",[815,1071,1024],{"class":825},[815,1073,1037],{"class":932},[815,1075,1063],{"class":825},[815,1077,1078],{"class":932},"user_id\n",[815,1080,1082,1085,1087,1089,1091,1094,1096,1098,1101,1103],{"class":817,"line":1081},6,[815,1083,1084],{"class":825},"            ||",[815,1086,1024],{"class":825},[815,1088,1027],{"class":932},[815,1090,1063],{"class":825},[815,1092,1093],{"class":829},"hasRole",[815,1095,833],{"class":825},[815,1097,836],{"class":825},[815,1099,1100],{"class":839},"admin",[815,1102,836],{"class":825},[815,1104,1105],{"class":825},");\n",[815,1107,1109],{"class":817,"line":1108},7,[815,1110,1111],{"class":825},"    }\n",[815,1113,1115],{"class":817,"line":1114},8,[815,1116,980],{"class":825},[805,1118,1121],{"className":879,"code":1119,"filename":1120,"language":882,"meta":811,"style":811},"func New() *kitpolicy.Registry {\n    reg := kitpolicy.NewRegistry()\n    reg.Register(\"orders.view\", func(ctx context.Context, id *rbac.Identity, req any) error {\n        if id.HasRole(\"admin\") {\n            return nil\n        }\n        r := req.(gen.GetOrderRequest)\n        if !ownsOrder(ctx, id, r.OrderId) {\n            return kitpolicy.Deny(\"You can only view your own orders.\")\n        }\n        return nil\n    })\n    return reg\n}\n","gpsystem: internal\u002Fmodules\u002Fshop\u002Fsurfaces\u002Fapi\u002Fpolicy\u002Fpolicy.go",[360,1122,1123,1145,1164,1229,1253,1261,1266,1288,1320,1343,1348,1355,1361,1370],{"__ignoreMap":811},[815,1124,1125,1127,1130,1133,1135,1138,1140,1143],{"class":817,"line":818},[815,1126,889],{"class":825},[815,1128,1129],{"class":829}," New",[815,1131,1132],{"class":825},"()",[815,1134,901],{"class":825},[815,1136,1137],{"class":821},"kitpolicy",[815,1139,907],{"class":825},[815,1141,1142],{"class":821},"Registry",[815,1144,927],{"class":825},[815,1146,1147,1150,1153,1156,1158,1161],{"class":817,"line":848},[815,1148,1149],{"class":932},"    reg ",[815,1151,1152],{"class":825},":=",[815,1154,1155],{"class":932}," kitpolicy",[815,1157,907],{"class":825},[815,1159,1160],{"class":829},"NewRegistry",[815,1162,1163],{"class":825},"()\n",[815,1165,1166,1169,1171,1174,1176,1178,1181,1183,1185,1188,1191,1194,1196,1199,1201,1204,1206,1209,1211,1214,1216,1219,1222,1224,1227],{"class":817,"line":868},[815,1167,1168],{"class":932},"    reg",[815,1170,907],{"class":825},[815,1172,1173],{"class":829},"Register",[815,1175,833],{"class":825},[815,1177,943],{"class":825},[815,1179,1180],{"class":839},"orders.view",[815,1182,943],{"class":825},[815,1184,912],{"class":825},[815,1186,1187],{"class":825}," func(",[815,1189,1190],{"class":897},"ctx",[815,1192,1193],{"class":821}," context",[815,1195,907],{"class":825},[815,1197,1198],{"class":821},"Context",[815,1200,912],{"class":825},[815,1202,1203],{"class":897}," id",[815,1205,901],{"class":825},[815,1207,1208],{"class":821},"rbac",[815,1210,907],{"class":825},[815,1212,1213],{"class":821},"Identity",[815,1215,912],{"class":825},[815,1217,1218],{"class":897}," req",[815,1220,1221],{"class":821}," any",[815,1223,404],{"class":825},[815,1225,1226],{"class":996}," error",[815,1228,927],{"class":825},[815,1230,1231,1234,1236,1238,1241,1243,1245,1247,1249,1251],{"class":817,"line":977},[815,1232,1233],{"class":1055},"        if",[815,1235,1203],{"class":932},[815,1237,907],{"class":825},[815,1239,1240],{"class":829},"HasRole",[815,1242,833],{"class":825},[815,1244,943],{"class":825},[815,1246,1100],{"class":839},[815,1248,943],{"class":825},[815,1250,404],{"class":825},[815,1252,927],{"class":825},[815,1254,1255,1258],{"class":817,"line":1052},[815,1256,1257],{"class":1055},"            return",[815,1259,1260],{"class":825}," nil\n",[815,1262,1263],{"class":817,"line":1081},[815,1264,1265],{"class":825},"        }\n",[815,1267,1268,1271,1273,1275,1278,1281,1283,1286],{"class":817,"line":1108},[815,1269,1270],{"class":932},"        r ",[815,1272,1152],{"class":825},[815,1274,1218],{"class":932},[815,1276,1277],{"class":825},".(",[815,1279,1280],{"class":821},"gen",[815,1282,907],{"class":825},[815,1284,1285],{"class":821},"GetOrderRequest",[815,1287,845],{"class":825},[815,1289,1290,1292,1295,1298,1300,1302,1304,1306,1308,1311,1313,1316,1318],{"class":817,"line":1114},[815,1291,1233],{"class":1055},[815,1293,1294],{"class":825}," !",[815,1296,1297],{"class":829},"ownsOrder",[815,1299,833],{"class":825},[815,1301,1190],{"class":932},[815,1303,912],{"class":825},[815,1305,1203],{"class":932},[815,1307,912],{"class":825},[815,1309,1310],{"class":932}," r",[815,1312,907],{"class":825},[815,1314,1315],{"class":932},"OrderId",[815,1317,404],{"class":825},[815,1319,927],{"class":825},[815,1321,1323,1325,1327,1329,1332,1334,1336,1339,1341],{"class":817,"line":1322},9,[815,1324,1257],{"class":1055},[815,1326,1155],{"class":932},[815,1328,907],{"class":825},[815,1330,1331],{"class":829},"Deny",[815,1333,833],{"class":825},[815,1335,943],{"class":825},[815,1337,1338],{"class":839},"You can only view your own orders.",[815,1340,943],{"class":825},[815,1342,845],{"class":825},[815,1344,1346],{"class":817,"line":1345},10,[815,1347,1265],{"class":825},[815,1349,1351,1353],{"class":817,"line":1350},11,[815,1352,1056],{"class":1055},[815,1354,1260],{"class":825},[815,1356,1358],{"class":817,"line":1357},12,[815,1359,1360],{"class":825},"    })\n",[815,1362,1364,1367],{"class":817,"line":1363},13,[815,1365,1366],{"class":1055},"    return",[815,1368,1369],{"class":932}," reg\n",[815,1371,1373],{"class":817,"line":1372},14,[815,1374,980],{"class":825},[348,1376,1377,1378,1380,1381,1384,1385,1388],{},"One substantive difference: in Laravel you call the policy from the controller (",[360,1379,478],{},"), and it can be forgotten. Here the policy is attached to the operation by the ",[360,1382,1383],{},"@policy"," decorator in the TypeSpec contract, and the enforcer middleware enforces it from the generated table. An operation that is tagged but has no registered policy doesn't slip through, it errors (",[421,1386,1387],{"href":214},"fail-closed",").",[369,1390,1392],{"id":1391},"behavioral-differences","Behavioral differences",[348,1394,1395,1396,1398],{},"The mapping table shows what corresponds to what. This section shows where the behavior is ",[352,1397,354],{}," the same once you look past the surface similarity.",[1400,1401,1402,1434,1458,1470,1482],"ul",{},[1403,1404,1405,1408,1409,1412,1413,1416,1417,1420,1421,1424,1425,1427,1428,1431,1432,907],"li",{},[352,1406,1407],{},"Transactions."," In Laravel, the transaction is tracked by a global connection manager: any model call automatically joins it, and ",[360,1410,1411],{},"DB::transaction"," retries on deadlock (the ",[360,1414,1415],{},"attempts"," parameter); rollback is triggered by an exception. In gpsystem the transaction travels in the ",[352,1418,1419],{},"context"," (",[360,1422,1423],{},"Transactor.WithinTransaction","): only a call that receives the closure's ",[360,1426,1190],{}," participates; there's no automatic deadlock retry (that decision is yours); rollback is triggered by returning a plain ",[360,1429,1430],{},"error",", not an exception. Details: ",[421,1433,152],{"href":153},[1403,1435,1436,1439,1440,1443,1444,1447,1448,1451,1452,1455,1456,907],{},[352,1437,1438],{},"Migrations."," The ",[360,1441,1442],{},"goose_db_version"," table is the counterpart of Laravel's ",[360,1445,1446],{},"migrations"," table, but there's no schema builder: plain SQL runs. This is a deliberate trade-off: the builder buys cross-database portability that isn't needed here (the kit is built on PostgreSQL), at the cost of hiding the actual DDL. Plain SQL exposes the full surface of PostgreSQL (partial indexes, ",[360,1449,1450],{},"GENERATED"," columns, CTE-based backfills) that a schema builder could only reach via ",[360,1453,1454],{},"DB::statement",". Details: ",[421,1457,157],{"href":158},[1403,1459,1460,1463,1464,1467,1468,907],{},[352,1461,1462],{},"Events and fan-out."," gpsystem treats every listener as an ",[352,1465,1466],{},"independent asynq task with its own retry budget",": one event fans out into N listener tasks, and a failing listener's retry never re-runs the others. That's the essential departure from Laravel's synchronous event loop (where one listener's exception can halt the rest), and the price is an idempotency requirement on every listener. Details: ",[421,1469,177],{"href":178},[1403,1471,1472,1475,1476,1479,1480,907],{},[352,1473,1474],{},"Transactional outbox."," Laravel has no built-in answer to the dual-write problem: ",[360,1477,1478],{},"afterCommit"," handles the rollback case, but doesn't close the post-commit crash window (the process dying between the DB commit and the event enqueue); anyone wanting a guarantee pulls in a separate outbox package there too. gpsystem ships this by default: the outbox writes the event into the same database, in the same transaction, as the business change. Details: ",[421,1481,182],{"href":183},[1403,1483,1484,1487,1488,907],{},[352,1485,1486],{},"OpenTelemetry."," There's no real PHP equivalent for this: in the Laravel ecosystem, Telescope is dev-only and production APM is the territory of paid services (Blackfire, New Relic). In the Go ecosystem, distributed tracing is standard, vendor-neutral infrastructure, which the kit switches on with one call. Details: ",[421,1489,232],{"href":233},[369,1491,1493],{"id":1492},"whats-missing-and-why","What's missing, and why",[1400,1495,1496,1505,1519],{},[1403,1497,1498,1501,1502,1504],{},[352,1499,1500],{},"No Eloquent."," The repository layer works with pgx (SQL) or bun (query builder), explicit queries are the established path in the Go ecosystem, and the kit doesn't try to simulate ActiveRecord. ",[421,1503,148],{"href":149}," is an optional adapter if you want query-builder comfort.",[1403,1506,1507,1439,1510,1512,1513,1515,1516,907],{},[352,1508,1509],{},"No service container.",[360,1511,362],{}," struct is built by hand in ",[360,1514,366],{},". In exchange, the answer to \"where does this dependency come from?\" is always a line in ",[360,1517,1518],{},"cmd\u002F\u003Cmodule>\u002Fmain.go",[1403,1520,1521,1524],{},[352,1522,1523],{},"No Blade."," The kit focuses on API backends; server-side rendering is not part of it.",[369,1526,1528],{"id":1527},"background","Background",[348,1530,1531,1532,1535,1536,1539,1540,1420,1543,1549],{},"The kit author's motivation was precisely to have ",[352,1533,1534],{},"a system in the Go ecosystem that is as easy to pick up as Laravel",": one where on day one of a project you write business logic, not evaluate infrastructure. The Go world's strength lies in small, independent, well-tested libraries. gpsystem doesn't want to replace them, but to offer a single, reconciled and maintained composition of them, in the conceptual frame you know from Laravel. That's why you see the parallels everywhere, and that's why the kit doesn't hide the libraries underneath: the library half is a ",[352,1537,1538],{},"microservice chassis",", the generated project a ",[352,1541,1542],{},"service template",[421,1544,1548],{"href":1545,"rel":1546},"https:\u002F\u002Fmicroservices.io\u002Fpatterns\u002Fmicroservice-chassis.html",[1547],"nofollow","Richardson","), not a runtime layer living above your app. The goal is a comfortable start, not a new, closed framework.",[1551,1552,1553],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":811,"searchDepth":848,"depth":848,"links":1555},[1556,1557,1558,1559,1560],{"id":371,"depth":848,"text":372},{"id":792,"depth":848,"text":793},{"id":1391,"depth":848,"text":1392},{"id":1492,"depth":848,"text":1493},{"id":1527,"depth":848,"text":1528},"A concept-by-concept mapping between Laravel and gpsystem.","md",null,{},{"icon":19},{"title":16,"description":1561},"h1eYSbt4ldRlIdyNd32opgrmh-VkMPI3eCwMNrGy32w",[1569,1571],{"title":11,"path":12,"stem":13,"description":1570,"icon":14,"children":-1},"What gpsystem is, who it's for, and what it leaves in your hands.",{"title":21,"path":22,"stem":23,"description":1572,"icon":19,"children":-1},"A concept-by-concept mapping between Symfony and gpsystem.",1784668701128]