[{"data":1,"prerenderedAt":2749},["ShallowReactive",2],{"navigation_docs":3,"-guide-database":93,"-guide-database-surround":2744},[4,71],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":70},"Guide","i-lucide-book-open","\u002Fguide","1.guide",[10,14,18,22,26,30,34,38,42,46,50,54,58,62,66],{"title":11,"path":12,"stem":13},"Getting started","\u002Fguide\u002Fgetting-started","1.guide\u002F01.getting-started",{"title":15,"path":16,"stem":17},"Adoption paths","\u002Fguide\u002Fadoption","1.guide\u002F02.adoption",{"title":19,"path":20,"stem":21},"Routes and typed $fetch","\u002Fguide\u002Froutes","1.guide\u002F02.routes",{"title":23,"path":24,"stem":25},"Kyo handlers","\u002Fguide\u002Fkyo","1.guide\u002F03.kyo",{"title":27,"path":28,"stem":29},"ZIO handlers","\u002Fguide\u002Fzio","1.guide\u002F04.zio",{"title":31,"path":32,"stem":33},"Configuration and secrets","\u002Fguide\u002Fconfiguration","1.guide\u002F05.configuration",{"title":35,"path":36,"stem":37},"Application and request lifetimes","\u002Fguide\u002Flifetimes","1.guide\u002F06.lifetimes",{"title":39,"path":40,"stem":41},"Schemas","\u002Fguide\u002Fschema","1.guide\u002F07.schema",{"title":43,"path":44,"stem":45},"PostgreSQL","\u002Fguide\u002Fdatabase","1.guide\u002F08.database",{"title":47,"path":48,"stem":49},"Streaming and cancellation","\u002Fguide\u002Fstreaming","1.guide\u002F09.streaming",{"title":51,"path":52,"stem":53},"Testing","\u002Fguide\u002Ftesting","1.guide\u002F10.testing",{"title":55,"path":56,"stem":57},"Nitro storage and NuxtHub KV","\u002Fguide\u002Fstorage","1.guide\u002F11.storage",{"title":59,"path":60,"stem":61},"Debug Scala handlers","\u002Fguide\u002Fdebugging","1.guide\u002F12.debugging",{"title":63,"path":64,"stem":65},"npm build tooling","\u002Fguide\u002Fnpm-tooling","1.guide\u002F13.npm-tooling",{"title":67,"path":68,"stem":69},"Set up h34s with a coding assistant","\u002Fguide\u002Fagent-setup","1.guide\u002F14.agent-setup",false,{"title":72,"icon":73,"path":74,"stem":75,"children":76,"page":70},"Reference","i-lucide-library","\u002Freference","2.reference",[77,81,85,89],{"title":78,"path":79,"stem":80},"Architecture","\u002Freference\u002Farchitecture","2.reference\u002F1.architecture",{"title":82,"path":83,"stem":84},"Releases","\u002Freference\u002Freleases","2.reference\u002F2.releases",{"title":86,"path":87,"stem":88},"Compatibility","\u002Freference\u002Fcompatibility","2.reference\u002F3.compatibility",{"title":90,"path":91,"stem":92},"Migration guide","\u002Freference\u002Fmigration","2.reference\u002F4.migration",{"id":94,"title":43,"body":95,"description":2738,"extension":2739,"links":2740,"meta":2741,"navigation":337,"path":44,"seo":2742,"stem":45,"__hash__":2743},"docs\u002F1.guide\u002F08.database.md",{"type":96,"value":97,"toc":2718},"minimark",[98,102,121,133,138,178,181,185,229,232,270,273,278,289,422,431,434,502,506,723,740,750,754,757,829,845,849,852,1392,1443,1449,1581,1585,1594,1605,1628,1631,1644,1690,1705,1712,1741,1745,1771,1775,1781,2171,2186,2190,2204,2617,2623,2652,2656,2674,2678,2694,2698,2714],[99,100,101],"p",{},"h34s has no database module, on purpose. Good PostgreSQL clients already run inside Scala.js on Node: use kyo-sql in a Kyo application and Skunk in a ZIO application, directly. h34s gives you the two pieces that connect them to handlers:",[103,104,105,118],"ul",{},[106,107,108,109,113,114,117],"li",{},"an application-scoped pool: ",[110,111,112],"code",{},"Shared"," (Kyo) or ",[110,115,116],{},"ZShared"," (ZIO), built on first use and released when Nitro closes;",[106,119,120],{},"a request scope for one session or transaction per request.",[99,122,123,124,128,129,132],{},"Your models, SQL, migrations and error mapping stay in your application. See ",[125,126,127],"a",{"href":36},"lifetimes"," and ",[125,130,131],{"href":32},"configuration"," for the h34s side.",[134,135,137],"h2",{"id":136},"the-pattern","The pattern",[139,140,141,159,168,171],"ol",{},[106,142,143,144,147,148,113,151,154,155,158],{},"Declare the connection settings in ",[110,145,146],{},"nuxt.config.ts"," and read them with ",[110,149,150],{},"RuntimeConfig.load",[110,152,153],{},"ZRuntimeConfig.load"," (ZIO). Keep the password in a ",[110,156,157],{},"Secret",", supplied at runtime.",[106,160,161,162,164,165,167],{},"Open one pool per application inside ",[110,163,112],{}," or ",[110,166,116],{},".",[106,169,170],{},"In each request, run your statements on that pool: one transaction (kyo-sql) or one pooled session (Skunk), released with the request scope.",[106,172,173,174,177],{},"Map database errors that the client may see, such as a unique-constraint violation, to your own ",[110,175,176],{},"SafeError"," inside the service. Let every other database error stay a defect: an opaque 500.",[99,179,180],{},"Create the schema with your own migration tool or scripts. Handlers should never create or change tables.",[134,182,184],{"id":183},"kyo-kyo-sql-and-kyo-sql-postgres","Kyo: kyo-sql and kyo-sql-postgres",[99,186,187,191,192,195,196,195,199,195,202,205,206,209,210,212,213,217,218,128,221,224,225,228],{},[188,189,190],"strong",{},"Status: runs against PostgreSQL 16 in Node; not run inside Nitro."," The service code below (",[110,193,194],{},"NotesLive",", ",[110,197,198],{},"Note",[110,200,201],{},"NoteView",[110,203,204],{},"DuplicateNote",") is copied without changes into the Kyo example's opt-in test, ",[110,207,208],{},"KyoSqlPostgresSuite",". On 2026-10-19, with kyo-sql-postgres 1.0.0-RC7, it inserted and listed a note, mapped a unique violation to ",[110,211,204],{},", rolled back two failed transactions, and kept the outer work of a transaction whose nested transaction failed, against PostgreSQL 16.15 with SCRAM authentication. See ",[125,214,216],{"href":215},"#run-the-kyo-sql-tests","Run the kyo-sql tests",". The ",[110,219,220],{},"Database",[110,222,223],{},"AppHandler"," snippets compiled in a temporary test file on 2026-10-17 (see ",[125,226,227],{"href":87},"compatibility","); they were not run, because they need Nitro. The demo's Nuxt application does not use kyo-sql. Treat this code as a starting point.",[99,230,231],{},"Add the client to your application:",[233,234,239],"pre",{"className":235,"code":236,"language":237,"meta":238,"style":238},"language-scala shiki shiki-themes github-light github-light github-dark","libraryDependencies += \"io.getkyo\" %%% \"kyo-sql-postgres\" % \"1.0.0-RC7\"\n","scala","",[110,240,241],{"__ignoreMap":238},[242,243,246,250,254,258,261,264,267],"span",{"class":244,"line":245},"line",1,[242,247,249],{"class":248},"sxrX7","libraryDependencies ",[242,251,253],{"class":252},"s8jYJ","+=",[242,255,257],{"class":256},"sIIMD"," \"io.getkyo\"",[242,259,260],{"class":252}," %%%",[242,262,263],{"class":256}," \"kyo-sql-postgres\"",[242,265,266],{"class":252}," %",[242,268,269],{"class":256}," \"1.0.0-RC7\"\n",[99,271,272],{},"It brings kyo-sql, kyo-net and kyo-schema-json with it.",[274,275,277],"h3",{"id":276},"configure-the-url","Configure the URL",[99,279,280,281,284,285,288],{},"kyo-sql takes one URL such as ",[110,282,283],{},"postgres:\u002F\u002Fuser:password@host:5432\u002Fdb",". Declare it as a private runtime config key, empty by default, and set ",[110,286,287],{},"NUXT_DATABASE_URL"," at runtime:",[233,290,292],{"className":235,"code":291,"language":237,"meta":238,"style":238},"import dev.yuri4n.exts.ntcvkyoschema.{NativeSchema, secretSchema}\n\nfinal case class DatabaseConfig(url: Secret, verifyTls: Boolean) derives Schema, NativeSchema\nfinal case class AppConfig(database: DatabaseConfig) derives Schema, NativeSchema\n",[110,293,294,332,339,390],{"__ignoreMap":238},[242,295,296,299,303,305,308,310,313,315,318,321,324,326,329],{"class":244,"line":245},[242,297,298],{"class":252},"import",[242,300,302],{"class":301},"snPdu"," dev",[242,304,167],{"class":248},[242,306,307],{"class":301},"yuri4n",[242,309,167],{"class":248},[242,311,312],{"class":301},"exts",[242,314,167],{"class":248},[242,316,317],{"class":301},"ntcvkyoschema",[242,319,320],{"class":248},".{",[242,322,323],{"class":301},"NativeSchema",[242,325,195],{"class":248},[242,327,328],{"class":301},"secretSchema",[242,330,331],{"class":248},"}\n",[242,333,335],{"class":244,"line":334},2,[242,336,338],{"emptyLinePlaceholder":337},true,"\n",[242,340,342,345,348,351,354,357,361,364,366,368,371,373,376,379,382,385,387],{"class":244,"line":341},3,[242,343,344],{"class":252},"final",[242,346,347],{"class":252}," case",[242,349,350],{"class":252}," class",[242,352,353],{"class":301}," DatabaseConfig",[242,355,356],{"class":248},"(",[242,358,360],{"class":359},"sP4rz","url",[242,362,363],{"class":248},": ",[242,365,157],{"class":301},[242,367,195],{"class":248},[242,369,370],{"class":359},"verifyTls",[242,372,363],{"class":248},[242,374,375],{"class":301},"Boolean",[242,377,378],{"class":248},") ",[242,380,381],{"class":252},"derives",[242,383,384],{"class":301}," Schema",[242,386,195],{"class":248},[242,388,389],{"class":301},"NativeSchema\n",[242,391,393,395,397,399,402,404,407,409,412,414,416,418,420],{"class":244,"line":392},4,[242,394,344],{"class":252},[242,396,347],{"class":252},[242,398,350],{"class":252},[242,400,401],{"class":301}," AppConfig",[242,403,356],{"class":248},[242,405,406],{"class":359},"database",[242,408,363],{"class":248},[242,410,411],{"class":301},"DatabaseConfig",[242,413,378],{"class":248},[242,415,381],{"class":252},[242,417,384],{"class":301},[242,419,195],{"class":248},[242,421,389],{"class":301},[99,423,424,426,427,430],{},[110,425,328],{}," is the bridge's ",[110,428,429],{},"Schema[Secret]",": import it where you derive a schema that holds a secret.",[99,432,433],{},"kyo-sql 1.0.0-RC7 parses this URL itself. Read these rules before you copy a URL from a hosting provider (they come from the kyo-sql source and are not tested by h34s):",[103,435,436,454,459,469],{},[106,437,438,441,442,445,446,449,450,453],{},[188,439,440],{},"The port is required."," ",[110,443,444],{},"postgres:\u002F\u002Fu:p@host\u002Fdb"," fails with ",[110,447,448],{},"SqlConnectionUrlParseException",". Add ",[110,451,452],{},":5432"," when your provider leaves it out.",[106,455,456],{},[188,457,458],{},"The database name is required.",[106,460,461,464,465,468],{},[188,462,463],{},"Credentials are not percent-decoded."," A password written as ",[110,466,467],{},"p%40ss"," is sent as those exact characters. Use a password that needs no encoding, or decode it before you build the URL.",[106,470,471,441,477,195,480,128,483,486,487,128,490,493,494,497,498,501],{},[188,472,473,476],{},[110,474,475],{},"sslmode=require"," encrypts but does not verify.",[110,478,479],{},"allow",[110,481,482],{},"prefer",[110,484,485],{},"require"," accept any certificate and host name. ",[110,488,489],{},"verify-ca",[110,491,492],{},"verify-full"," need ",[110,495,496],{},"sslrootcert",", a file path. To verify against the system certificate store without a file, set the TLS settings in code, as below. Settings in code win over the URL's trust choice; an ",[110,499,500],{},"sslmode"," in the URL still wins over the mode in code.",[274,503,505],{"id":504},"open-one-pool-for-the-application","Open one pool for the application",[233,507,509],{"className":235,"code":508,"language":237,"meta":238,"style":238},"import dev.yuri4n.exts.kyoh3.*\nimport kyo.*\nimport kyo.net.NetTlsConfig\n\nobject Database:\n    \u002F** One pool for all requests: opened on first use, closed when Nitro closes. *\u002F\n    val client: Shared[PostgresClient] = Shared(\n      RuntimeConfig.load[AppConfig].map { config =>\n        val sqlConfig =\n          if config.database.verifyTls then\n              SqlConfig.default.tlsMode(SqlConfig.TlsMode.Require).tls(NetTlsConfig.default)\n          else SqlConfig.default\n        PostgresClient.init(config.database.url.reveal, sqlConfig)\n      },\n    )\n",[110,510,511,535,546,562,566,578,585,617,635,647,659,690,702,711,717],{"__ignoreMap":238},[242,512,513,515,517,519,521,523,525,527,530,532],{"class":244,"line":245},[242,514,298],{"class":252},[242,516,302],{"class":301},[242,518,167],{"class":248},[242,520,307],{"class":301},[242,522,167],{"class":248},[242,524,312],{"class":301},[242,526,167],{"class":248},[242,528,529],{"class":301},"kyoh3",[242,531,167],{"class":248},[242,533,534],{"class":301},"*\n",[242,536,537,539,542,544],{"class":244,"line":334},[242,538,298],{"class":252},[242,540,541],{"class":301}," kyo",[242,543,167],{"class":248},[242,545,534],{"class":301},[242,547,548,550,552,554,557,559],{"class":244,"line":341},[242,549,298],{"class":252},[242,551,541],{"class":301},[242,553,167],{"class":248},[242,555,556],{"class":301},"net",[242,558,167],{"class":248},[242,560,561],{"class":301},"NetTlsConfig\n",[242,563,564],{"class":244,"line":392},[242,565,338],{"emptyLinePlaceholder":337},[242,567,569,572,575],{"class":244,"line":568},5,[242,570,571],{"class":252},"object",[242,573,574],{"class":301}," Database",[242,576,577],{"class":252},":\n",[242,579,581],{"class":244,"line":580},6,[242,582,584],{"class":583},"sCsY4","    \u002F** One pool for all requests: opened on first use, closed when Nitro closes. *\u002F\n",[242,586,588,591,594,597,600,603,606,609,612,614],{"class":244,"line":587},7,[242,589,590],{"class":252},"    val",[242,592,593],{"class":359}," client",[242,595,596],{"class":252},":",[242,598,599],{"class":301}," Shared",[242,601,602],{"class":248},"[",[242,604,605],{"class":301},"PostgresClient",[242,607,608],{"class":248},"] ",[242,610,611],{"class":252},"=",[242,613,599],{"class":301},[242,615,616],{"class":248},"(\n",[242,618,620,623,626,629,632],{"class":244,"line":619},8,[242,621,622],{"class":301},"      RuntimeConfig",[242,624,625],{"class":248},".load[",[242,627,628],{"class":301},"AppConfig",[242,630,631],{"class":248},"].map { config ",[242,633,634],{"class":252},"=>\n",[242,636,638,641,644],{"class":244,"line":637},9,[242,639,640],{"class":252},"        val",[242,642,643],{"class":359}," sqlConfig",[242,645,646],{"class":252}," =\n",[242,648,650,653,656],{"class":244,"line":649},10,[242,651,652],{"class":252},"          if",[242,654,655],{"class":248}," config.database.verifyTls ",[242,657,658],{"class":252},"then\n",[242,660,662,665,668,671,673,676,678,681,684,687],{"class":244,"line":661},11,[242,663,664],{"class":301},"              SqlConfig",[242,666,667],{"class":248},".default.tlsMode(",[242,669,670],{"class":301},"SqlConfig",[242,672,167],{"class":248},[242,674,675],{"class":301},"TlsMode",[242,677,167],{"class":248},[242,679,680],{"class":301},"Require",[242,682,683],{"class":248},").tls(",[242,685,686],{"class":301},"NetTlsConfig",[242,688,689],{"class":248},".default)\n",[242,691,693,696,699],{"class":244,"line":692},12,[242,694,695],{"class":252},"          else",[242,697,698],{"class":301}," SqlConfig",[242,700,701],{"class":248},".default\n",[242,703,705,708],{"class":244,"line":704},13,[242,706,707],{"class":301},"        PostgresClient",[242,709,710],{"class":248},".init(config.database.url.reveal, sqlConfig)\n",[242,712,714],{"class":244,"line":713},14,[242,715,716],{"class":248},"      },\n",[242,718,720],{"class":244,"line":719},15,[242,721,722],{"class":248},"    )\n",[99,724,725,728,729,732,733,735,736,739],{},[110,726,727],{},"PostgresClient.init"," registers the pool in the ",[110,730,731],{},"Scope"," it runs in. Inside ",[110,734,112],{},", that is the resource's own scope, so the pool closes when Nitro closes. Do not call ",[110,737,738],{},"DB.run(url)"," per request: it opens and closes a whole pool on every call.",[99,741,742,743,745,746,749],{},"Set ",[110,744,370],{}," to ",[110,747,748],{},"false"," only for a local database without TLS.",[274,751,753],{"id":752},"create-the-table","Create the table",[99,755,756],{},"Create the table with your migration tool or setup script, not in a handler. The code below expects this table:",[233,758,762],{"className":759,"code":760,"language":761,"meta":238,"style":238},"language-sql shiki shiki-themes github-light github-light github-dark","CREATE TABLE IF NOT EXISTS notes (\n  id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  text text NOT NULL UNIQUE\n);\n","sql",[110,763,764,784,810,824],{"__ignoreMap":238},[242,765,766,769,772,775,778,781],{"class":244,"line":245},[242,767,768],{"class":252},"CREATE",[242,770,771],{"class":252}," TABLE",[242,773,774],{"class":301}," IF",[242,776,777],{"class":252}," NOT",[242,779,780],{"class":252}," EXISTS",[242,782,783],{"class":248}," notes (\n",[242,785,786,789,792,795,798,801,804,807],{"class":244,"line":334},[242,787,788],{"class":248},"  id ",[242,790,791],{"class":252},"integer",[242,793,794],{"class":252}," GENERATED",[242,796,797],{"class":252}," ALWAYS",[242,799,800],{"class":252}," AS",[242,802,803],{"class":252}," IDENTITY",[242,805,806],{"class":252}," PRIMARY KEY",[242,808,809],{"class":248},",\n",[242,811,812,815,818,821],{"class":244,"line":341},[242,813,814],{"class":252},"  text",[242,816,817],{"class":252}," text",[242,819,820],{"class":252}," NOT NULL",[242,822,823],{"class":252}," UNIQUE\n",[242,825,826],{"class":244,"line":392},[242,827,828],{"class":248},");\n",[99,830,831,834,835,838,839,842,843,167],{},[110,832,833],{},"UNIQUE"," gives the ",[110,836,837],{},"23505"," violation that ",[110,840,841],{},"create"," maps to ",[110,844,204],{},[274,846,848],{"id":847},"run-statements-per-request","Run statements per request",[99,850,851],{},"Keep SQL in your service and map the errors a client may see. Keep three models apart: the table row (read by kyo-sql), the domain note (what the service returns), and the JSON that a route sends. A new column then reaches the JSON only when you add it to the wire model.",[233,853,855],{"className":235,"code":854,"language":237,"meta":238,"style":238},"\u002F** The domain note. It has no table or wire format. *\u002F\nfinal case class Note(id: Int, text: String)\n\n\u002F** The JSON that the Notes routes send. *\u002F\nfinal case class NoteView(id: Int, text: String) derives Schema, NativeSchema\n\nobject NoteView:\n    def from(note: Note): NoteView = NoteView(note.id, note.text)\n\nfinal case class DuplicateNote() extends SafeError:\n    def message: String      = \"A note with this text already exists\"\n    override def status: Int = 409\n\nfinal class NotesLive(client: PostgresClient):\n    import NotesLive.NoteRow\n\n    def create(text: String): Note \u003C (Async & Abort[DuplicateNote | SqlException]) =\n      Abort.recover[SqlServerConstraintViolationException](error =>\n        if error.sqlState == \"23505\" then Abort.fail(DuplicateNote()) else Abort.fail(error),\n      )(DB.run(client)(DB.transaction(\n        sql\"INSERT INTO notes (text) VALUES ($text) RETURNING id, text\".as[NoteRow].run.map(_.head.toNote),\n      )))\n\n    def list: Chunk[Note] \u003C (Async & Abort[SqlException]) =\n      DB.run(client)(sql\"SELECT id, text FROM notes ORDER BY id\".as[NoteRow].run.map(_.map(_.toNote)))\n\nobject NotesLive:\n    \u002F** The table row. It stays inside the service. *\u002F\n    private final case class NoteRow(id: Int, text: String) derives Schema:\n        def toNote: Note = Note(id, text)\n",[110,856,857,862,896,900,905,942,946,954,986,990,1012,1030,1052,1056,1078,1090,1095,1148,1165,1201,1218,1242,1248,1253,1292,1312,1317,1326,1332,1372],{"__ignoreMap":238},[242,858,859],{"class":244,"line":245},[242,860,861],{"class":583},"\u002F** The domain note. It has no table or wire format. *\u002F\n",[242,863,864,866,868,870,873,875,878,880,883,885,888,890,893],{"class":244,"line":334},[242,865,344],{"class":252},[242,867,347],{"class":252},[242,869,350],{"class":252},[242,871,872],{"class":301}," Note",[242,874,356],{"class":248},[242,876,877],{"class":359},"id",[242,879,363],{"class":248},[242,881,882],{"class":301},"Int",[242,884,195],{"class":248},[242,886,887],{"class":359},"text",[242,889,363],{"class":248},[242,891,892],{"class":301},"String",[242,894,895],{"class":248},")\n",[242,897,898],{"class":244,"line":341},[242,899,338],{"emptyLinePlaceholder":337},[242,901,902],{"class":244,"line":392},[242,903,904],{"class":583},"\u002F** The JSON that the Notes routes send. *\u002F\n",[242,906,907,909,911,913,916,918,920,922,924,926,928,930,932,934,936,938,940],{"class":244,"line":568},[242,908,344],{"class":252},[242,910,347],{"class":252},[242,912,350],{"class":252},[242,914,915],{"class":301}," NoteView",[242,917,356],{"class":248},[242,919,877],{"class":359},[242,921,363],{"class":248},[242,923,882],{"class":301},[242,925,195],{"class":248},[242,927,887],{"class":359},[242,929,363],{"class":248},[242,931,892],{"class":301},[242,933,378],{"class":248},[242,935,381],{"class":252},[242,937,384],{"class":301},[242,939,195],{"class":248},[242,941,389],{"class":301},[242,943,944],{"class":244,"line":580},[242,945,338],{"emptyLinePlaceholder":337},[242,947,948,950,952],{"class":244,"line":587},[242,949,571],{"class":252},[242,951,915],{"class":301},[242,953,577],{"class":252},[242,955,956,959,962,964,967,969,971,974,976,978,981,983],{"class":244,"line":619},[242,957,958],{"class":252},"    def",[242,960,961],{"class":301}," from",[242,963,356],{"class":248},[242,965,966],{"class":359},"note",[242,968,363],{"class":248},[242,970,198],{"class":301},[242,972,973],{"class":248},")",[242,975,596],{"class":252},[242,977,915],{"class":301},[242,979,980],{"class":252}," =",[242,982,915],{"class":301},[242,984,985],{"class":248},"(note.id, note.text)\n",[242,987,988],{"class":244,"line":637},[242,989,338],{"emptyLinePlaceholder":337},[242,991,992,994,996,998,1001,1004,1007,1010],{"class":244,"line":649},[242,993,344],{"class":252},[242,995,347],{"class":252},[242,997,350],{"class":252},[242,999,1000],{"class":301}," DuplicateNote",[242,1002,1003],{"class":248},"() ",[242,1005,1006],{"class":252},"extends",[242,1008,1009],{"class":301}," SafeError",[242,1011,577],{"class":252},[242,1013,1014,1016,1019,1021,1024,1027],{"class":244,"line":661},[242,1015,958],{"class":252},[242,1017,1018],{"class":301}," message",[242,1020,596],{"class":252},[242,1022,1023],{"class":301}," String",[242,1025,1026],{"class":252},"      =",[242,1028,1029],{"class":256}," \"A note with this text already exists\"\n",[242,1031,1032,1035,1038,1041,1043,1046,1048],{"class":244,"line":692},[242,1033,1034],{"class":252},"    override",[242,1036,1037],{"class":252}," def",[242,1039,1040],{"class":301}," status",[242,1042,596],{"class":252},[242,1044,1045],{"class":301}," Int",[242,1047,980],{"class":252},[242,1049,1051],{"class":1050},"sBjJW"," 409\n",[242,1053,1054],{"class":244,"line":704},[242,1055,338],{"emptyLinePlaceholder":337},[242,1057,1058,1060,1062,1065,1067,1070,1072,1074,1076],{"class":244,"line":713},[242,1059,344],{"class":252},[242,1061,350],{"class":252},[242,1063,1064],{"class":301}," NotesLive",[242,1066,356],{"class":248},[242,1068,1069],{"class":359},"client",[242,1071,363],{"class":248},[242,1073,605],{"class":301},[242,1075,973],{"class":248},[242,1077,577],{"class":252},[242,1079,1080,1083,1085,1087],{"class":244,"line":719},[242,1081,1082],{"class":252},"    import",[242,1084,1064],{"class":301},[242,1086,167],{"class":248},[242,1088,1089],{"class":301},"NoteRow\n",[242,1091,1093],{"class":244,"line":1092},16,[242,1094,338],{"emptyLinePlaceholder":337},[242,1096,1098,1100,1103,1105,1107,1109,1111,1113,1115,1117,1120,1123,1126,1129,1132,1134,1136,1139,1142,1145],{"class":244,"line":1097},17,[242,1099,958],{"class":252},[242,1101,1102],{"class":301}," create",[242,1104,356],{"class":248},[242,1106,887],{"class":359},[242,1108,363],{"class":248},[242,1110,892],{"class":301},[242,1112,973],{"class":248},[242,1114,596],{"class":252},[242,1116,872],{"class":301},[242,1118,1119],{"class":252}," \u003C",[242,1121,1122],{"class":248}," (",[242,1124,1125],{"class":301},"Async",[242,1127,1128],{"class":252}," &",[242,1130,1131],{"class":301}," Abort",[242,1133,602],{"class":248},[242,1135,204],{"class":301},[242,1137,1138],{"class":252}," |",[242,1140,1141],{"class":301}," SqlException",[242,1143,1144],{"class":248},"]) ",[242,1146,1147],{"class":252},"=\n",[242,1149,1151,1154,1157,1160,1163],{"class":244,"line":1150},18,[242,1152,1153],{"class":301},"      Abort",[242,1155,1156],{"class":248},".recover[",[242,1158,1159],{"class":301},"SqlServerConstraintViolationException",[242,1161,1162],{"class":248},"](error ",[242,1164,634],{"class":252},[242,1166,1168,1171,1174,1177,1180,1183,1185,1188,1190,1193,1196,1198],{"class":244,"line":1167},19,[242,1169,1170],{"class":252},"        if",[242,1172,1173],{"class":248}," error.sqlState ",[242,1175,1176],{"class":252},"==",[242,1178,1179],{"class":256}," \"23505\"",[242,1181,1182],{"class":252}," then",[242,1184,1131],{"class":301},[242,1186,1187],{"class":248},".fail(",[242,1189,204],{"class":301},[242,1191,1192],{"class":248},"()) ",[242,1194,1195],{"class":252},"else",[242,1197,1131],{"class":301},[242,1199,1200],{"class":248},".fail(error),\n",[242,1202,1204,1207,1210,1213,1215],{"class":244,"line":1203},20,[242,1205,1206],{"class":248},"      )(",[242,1208,1209],{"class":301},"DB",[242,1211,1212],{"class":248},".run(client)(",[242,1214,1209],{"class":301},[242,1216,1217],{"class":248},".transaction(\n",[242,1219,1221,1224,1227,1230,1233,1236,1239],{"class":244,"line":1220},21,[242,1222,1223],{"class":252},"        sql",[242,1225,1226],{"class":256},"\"INSERT INTO notes (text) VALUES (",[242,1228,1229],{"class":248},"$text",[242,1231,1232],{"class":256},") RETURNING id, text\"",[242,1234,1235],{"class":248},".as[",[242,1237,1238],{"class":301},"NoteRow",[242,1240,1241],{"class":248},"].run.map(_.head.toNote),\n",[242,1243,1245],{"class":244,"line":1244},22,[242,1246,1247],{"class":248},"      )))\n",[242,1249,1251],{"class":244,"line":1250},23,[242,1252,338],{"emptyLinePlaceholder":337},[242,1254,1256,1258,1261,1263,1266,1268,1270,1272,1275,1277,1279,1281,1283,1285,1288,1290],{"class":244,"line":1255},24,[242,1257,958],{"class":252},[242,1259,1260],{"class":301}," list",[242,1262,596],{"class":252},[242,1264,1265],{"class":301}," Chunk",[242,1267,602],{"class":248},[242,1269,198],{"class":301},[242,1271,608],{"class":248},[242,1273,1274],{"class":252},"\u003C",[242,1276,1122],{"class":248},[242,1278,1125],{"class":301},[242,1280,1128],{"class":252},[242,1282,1131],{"class":301},[242,1284,602],{"class":248},[242,1286,1287],{"class":301},"SqlException",[242,1289,1144],{"class":248},[242,1291,1147],{"class":252},[242,1293,1295,1298,1300,1302,1305,1307,1309],{"class":244,"line":1294},25,[242,1296,1297],{"class":301},"      DB",[242,1299,1212],{"class":248},[242,1301,761],{"class":252},[242,1303,1304],{"class":256},"\"SELECT id, text FROM notes ORDER BY id\"",[242,1306,1235],{"class":248},[242,1308,1238],{"class":301},[242,1310,1311],{"class":248},"].run.map(_.map(_.toNote)))\n",[242,1313,1315],{"class":244,"line":1314},26,[242,1316,338],{"emptyLinePlaceholder":337},[242,1318,1320,1322,1324],{"class":244,"line":1319},27,[242,1321,571],{"class":252},[242,1323,1064],{"class":301},[242,1325,577],{"class":252},[242,1327,1329],{"class":244,"line":1328},28,[242,1330,1331],{"class":583},"    \u002F** The table row. It stays inside the service. *\u002F\n",[242,1333,1335,1338,1341,1343,1345,1348,1350,1352,1354,1356,1358,1360,1362,1364,1366,1368,1370],{"class":244,"line":1334},29,[242,1336,1337],{"class":252},"    private",[242,1339,1340],{"class":252}," final",[242,1342,347],{"class":252},[242,1344,350],{"class":252},[242,1346,1347],{"class":301}," NoteRow",[242,1349,356],{"class":248},[242,1351,877],{"class":359},[242,1353,363],{"class":248},[242,1355,882],{"class":301},[242,1357,195],{"class":248},[242,1359,887],{"class":359},[242,1361,363],{"class":248},[242,1363,892],{"class":301},[242,1365,378],{"class":248},[242,1367,381],{"class":252},[242,1369,384],{"class":301},[242,1371,577],{"class":252},[242,1373,1375,1378,1381,1383,1385,1387,1389],{"class":244,"line":1374},30,[242,1376,1377],{"class":252},"        def",[242,1379,1380],{"class":301}," toNote",[242,1382,596],{"class":252},[242,1384,872],{"class":301},[242,1386,980],{"class":252},[242,1388,872],{"class":301},[242,1390,1391],{"class":248},"(id, text)\n",[103,1393,1394,1403,1431],{},[106,1395,1396,1399,1400,1402],{},[110,1397,1398],{},"sql\"...\""," binds ",[110,1401,1229],{}," as a parameter. The value is never concatenated into the SQL text.",[106,1404,1405,1408,1409,1412,1413,1416,1417,1419,1420,1422,1423,1426,1427,1430],{},[110,1406,1407],{},"DB.transaction"," commits when the body succeeds and rolls back when it fails, also on a non-SQL ",[110,1410,1411],{},"Abort",". A body that recovers from a failed statement still rolls back: the transaction then fails with ",[110,1414,1415],{},"SqlRequestTransactionFailedStatementException",". To keep the rest of the work, wrap the step that may fail in a nested ",[110,1418,1407],{}," and recover from its failure. The nested transaction takes a savepoint, so only its own part rolls back and the outer transaction still commits; ",[110,1421,208],{}," tests this. A client disconnect interrupts the transaction too, so it can roll back. When the insert must finish anyway, run it with ",[110,1424,1425],{},"KHandler.uninterruptible(notes.create(text))","; see ",[125,1428,127],{"href":1429},"\u002Fguide\u002Flifetimes#when-the-client-disconnects",". ",[106,1432,1433,1436,1437,1439,1440,1442],{},[110,1434,1435],{},"SQLSTATE 23505"," is a unique violation. Map only the states your client should see; ",[110,1438,1287],{}," details can contain values from the database, so never put them in a ",[110,1441,176],{}," message.",[99,1444,1445,1446,1448],{},"Provide the service in your ",[110,1447,223],{},", the same way as any Kyo service:",[233,1450,1452],{"className":235,"code":1451,"language":237,"meta":238,"style":238},"object AppHandler:\n    def define[B: ResponseBody](program: Request => B \u003C (HandlerEffects & Env[NotesLive]))(using Frame): EventHandler =\n      KHandler.define(HandlerOptions(defaults = ResponseHead.noStore)): request =>\n          Database.client.get.map(client => Env.run(NotesLive(client))(program(request)))\n",[110,1453,1454,1463,1531,1560],{"__ignoreMap":238},[242,1455,1456,1458,1461],{"class":244,"line":245},[242,1457,571],{"class":252},[242,1459,1460],{"class":301}," AppHandler",[242,1462,577],{"class":252},[242,1464,1465,1467,1470,1472,1475,1477,1480,1483,1486,1488,1491,1494,1497,1499,1501,1504,1506,1509,1511,1513,1516,1519,1522,1524,1526,1529],{"class":244,"line":334},[242,1466,958],{"class":252},[242,1468,1469],{"class":301}," define",[242,1471,602],{"class":248},[242,1473,1474],{"class":301},"B",[242,1476,596],{"class":252},[242,1478,1479],{"class":301}," ResponseBody",[242,1481,1482],{"class":248},"](",[242,1484,1485],{"class":359},"program",[242,1487,363],{"class":248},[242,1489,1490],{"class":301},"Request",[242,1492,1493],{"class":252}," =>",[242,1495,1496],{"class":301}," B",[242,1498,1119],{"class":252},[242,1500,1122],{"class":248},[242,1502,1503],{"class":301},"HandlerEffects",[242,1505,1128],{"class":252},[242,1507,1508],{"class":301}," Env",[242,1510,602],{"class":248},[242,1512,194],{"class":301},[242,1514,1515],{"class":248},"]))(",[242,1517,1518],{"class":252},"using",[242,1520,1521],{"class":301}," Frame",[242,1523,973],{"class":248},[242,1525,596],{"class":252},[242,1527,1528],{"class":301}," EventHandler",[242,1530,646],{"class":252},[242,1532,1533,1536,1539,1542,1545,1547,1550,1553,1555,1558],{"class":244,"line":341},[242,1534,1535],{"class":301},"      KHandler",[242,1537,1538],{"class":248},".define(",[242,1540,1541],{"class":301},"HandlerOptions",[242,1543,1544],{"class":248},"(defaults ",[242,1546,611],{"class":252},[242,1548,1549],{"class":301}," ResponseHead",[242,1551,1552],{"class":248},".noStore))",[242,1554,596],{"class":252},[242,1556,1557],{"class":248}," request ",[242,1559,634],{"class":252},[242,1561,1562,1565,1568,1571,1573,1576,1578],{"class":244,"line":392},[242,1563,1564],{"class":301},"          Database",[242,1566,1567],{"class":248},".client.get.map(client ",[242,1569,1570],{"class":252},"=>",[242,1572,1508],{"class":301},[242,1574,1575],{"class":248},".run(",[242,1577,194],{"class":301},[242,1579,1580],{"class":248},"(client))(program(request)))\n",[274,1582,1584],{"id":1583},"run-kyo-sql-inside-nitro","Run kyo-sql inside Nitro",[99,1586,1587,1588,128,1590,1593],{},"kyo-net picks an I\u002FO backend when it starts. It prefers native backends loaded through FFI and falls back to Node's ",[110,1589,556],{},[110,1591,1592],{},"tls",". In a Nitro server, force the Node backend:",[233,1595,1599],{"className":1596,"code":1597,"language":1598,"meta":238,"style":238},"language-sh shiki shiki-themes github-light github-light github-dark","KYO_NET_BACKEND=node node .output\u002Fserver\u002Findex.mjs\n","sh",[110,1600,1601],{"__ignoreMap":238},[242,1602,1603],{"class":244,"line":245},[242,1604,1597],{},[99,1606,1607,1610,1611,1614,1615,1618,1619,1622,1623,164,1625,1627],{},[188,1608,1609],{},"Unverified inside Nitro."," h34s has not run kyo-sql inside a Nitro bundle. In the Node test run, without ",[110,1612,1613],{},"KYO_NET_BACKEND",", kyo-net selected its ",[110,1616,1617],{},"node"," backend by itself and logged a warning that the FFI backends were unavailable, because the ",[110,1620,1621],{},"koffi"," npm package was not installed. The FFI backends, the bundle size, and non-Node Nitro presets (which have no ",[110,1624,556],{},[110,1626,1592],{},") are open questions. Test your deployment target before you rely on it.",[274,1629,216],{"id":1630},"run-the-kyo-sql-tests",[99,1632,1633,1635,1636,1639,1640,1643],{},[110,1634,208],{}," in ",[110,1637,1638],{},"examples\u002Fnuxt\u002Fscala\u002Fkyo\u002Fsrc\u002Ftest\u002Fscala\u002F"," runs the service code above against a real database. It runs only when ",[110,1641,1642],{},"H34S_POSTGRES_URL"," holds a kyo-sql URL. Without the variable, munit reports its five tests as skipped.",[139,1645,1646,1678],{},[106,1647,1648,1649,1652,1653,1655,1656,1659,1660,1666,1667,1670,1671,1673,1674,1677],{},"Start PostgreSQL 16 and create the role ",[110,1650,1651],{},"h34s",", the database ",[110,1654,1651],{}," and the password file ",[110,1657,1658],{},"~\u002F.h34s-db-password",", as in ",[125,1661,1665],{"href":1662,"rel":1663},"https:\u002F\u002Fgithub.com\u002Fyuri4n\u002Fh34s\u002Fblob\u002Fmain\u002Fdocs\u002Fmaintainers\u002Fcloud.md#start-postgresql-and-redis",[1664],"nofollow","Start PostgreSQL and Redis",". Use a database that has no ",[110,1668,1669],{},"notes"," table of your own: the suite creates ",[110,1672,1669],{}," if it does not exist, writes rows whose text starts with ",[110,1675,1676],{},"h34s kyo-sql ",", and deletes those rows when each test ends. The table stays.",[106,1679,1680,1681],{},"Run the suite from the repository root:",[233,1682,1684],{"className":1596,"code":1683,"language":1598,"meta":238,"style":238},"H34S_POSTGRES_URL=\"postgres:\u002F\u002Fh34s:$(cat ~\u002F.h34s-db-password)@127.0.0.1:5432\u002Fh34s\" sbt -batch kyoExample\u002Ftest\n",[110,1685,1686],{"__ignoreMap":238},[242,1687,1688],{"class":244,"line":245},[242,1689,1683],{},[99,1691,1692,1693,1696,1697,1700,1701,1704],{},"Expected result: ",[110,1694,1695],{},"Passed: Total 5, Failed 0, Errors 0, Passed 5",". The suite runs in Node through sbt, not inside Nitro. Set ",[110,1698,1699],{},"H34S_POSTGRES_REQUIRED=true"," to make the tests fail instead of skip when the URL is missing; the ",[110,1702,1703],{},"postgres"," CI job does this.",[134,1706,1708,1709,1711],{"id":1707},"zio-skunk-in-a-zshared-layer","ZIO: Skunk in a ",[110,1710,116],{}," layer",[99,1713,1714,1715,1718,1719,1722,1723,1726,1727,1730,1731,1734,1735,1740],{},"The demo's Notes example uses Skunk 1.0.0-M12 through zio-interop-cats. Its handlers are unit-tested. On 2026-10-19, a production build of the demo ran against PostgreSQL 16.15 with ",[110,1716,1717],{},"scram-sha-256"," authentication: ",[110,1720,1721],{},"POST \u002Fapi\u002Fnotes"," answered 201, invalid input answered 422, ",[110,1724,1725],{},"GET \u002Fapi\u002Fnotes"," listed the note, and after ",[110,1728,1729],{},"SIGTERM"," no session of the example's user stayed in ",[110,1732,1733],{},"pg_stat_activity",". See the ",[125,1736,1739],{"href":1737,"rel":1738},"https:\u002F\u002Fgithub.com\u002Fyuri4n\u002Fh34s\u002Fblob\u002Fmain\u002Fdocs\u002Fdevelopment.md#cloud-session-production-build-and-postgresql-2026-10-19",[1664],"development guide"," for the steps and the output.",[274,1742,1744],{"id":1743},"configure","Configure",[99,1746,1747,1748,1751,1752,1754,1755,195,1758,195,1761,195,1764,128,1767,1770],{},"The settings come from ",[110,1749,1750],{},"runtimeConfig.database"," (see ",[125,1753,131],{"href":32},"): ",[110,1756,1757],{},"NUXT_DATABASE_HOST",[110,1759,1760],{},"NUXT_DATABASE_PORT",[110,1762,1763],{},"NUXT_DATABASE_USER",[110,1765,1766],{},"NUXT_DATABASE_PASSWORD",[110,1768,1769],{},"NUXT_DATABASE_NAME",". An empty password means none.",[274,1772,1774],{"id":1773},"build-the-pool-once","Build the pool once",[99,1776,1777,1780],{},[110,1778,1779],{},"SessionPool.scala"," opens one Skunk pool from the startup config:",[233,1782,1784],{"className":235,"code":1783,"language":237,"meta":238,"style":238},"\u002F** Skunk sessions from one application-scoped pool. Each request takes its own session. *\u002F\nfinal class SessionPool(sessions: Resource[Task, Session[Task]]):\n    \u002F** A pooled session that returns to the pool when `Scope` closes (the request scope in a handler). *\u002F\n    def session: ZIO[Scope, Throwable, Session[Task]] = sessions.toScopedZIO\n\nobject SessionPool:\n    \u002F** Reads the startup runtime config and creates the pool. Connections open on first use. *\u002F\n    val layer: ZLayer[Any, Throwable, SessionPool] =\n      ZLayer.scoped(\n        for\n          config   \u003C- ZRuntimeConfig.load[AppConfig]\n          sessions \u003C- pooled(config.database).toScopedZIO\n        yield SessionPool(sessions),\n      )\n\n    private def pooled(config: DatabaseConfig): Resource[Task, Resource[Task, Session[Task]]] =\n      given Tracer[Task]  = Tracer.noop\n      given Console[Task] = Console.make[Task]\n      given Network[Task] = Network.forAsync[Task]\n      val password = Some(config.password.reveal).filter(_.nonEmpty)\n      Session.Builder[Task]\n        .withHost(config.host)\n        .withPort(config.port)\n        .withCredentials(Session.Credentials(config.user, password))\n        .withDatabase(config.name)\n        .pooled(max = 4)\n",[110,1785,1786,1791,1829,1834,1871,1875,1883,1888,1917,1925,1930,1948,1958,1968,1973,1977,2027,2049,2073,2097,2113,2129,2134,2139,2154,2159],{"__ignoreMap":238},[242,1787,1788],{"class":244,"line":245},[242,1789,1790],{"class":583},"\u002F** Skunk sessions from one application-scoped pool. Each request takes its own session. *\u002F\n",[242,1792,1793,1795,1797,1800,1802,1805,1807,1810,1812,1815,1817,1820,1822,1824,1827],{"class":244,"line":334},[242,1794,344],{"class":252},[242,1796,350],{"class":252},[242,1798,1799],{"class":301}," SessionPool",[242,1801,356],{"class":248},[242,1803,1804],{"class":359},"sessions",[242,1806,363],{"class":248},[242,1808,1809],{"class":301},"Resource",[242,1811,602],{"class":248},[242,1813,1814],{"class":301},"Task",[242,1816,195],{"class":248},[242,1818,1819],{"class":301},"Session",[242,1821,602],{"class":248},[242,1823,1814],{"class":301},[242,1825,1826],{"class":248},"]])",[242,1828,577],{"class":252},[242,1830,1831],{"class":244,"line":341},[242,1832,1833],{"class":583},"    \u002F** A pooled session that returns to the pool when `Scope` closes (the request scope in a handler). *\u002F\n",[242,1835,1836,1838,1841,1843,1846,1848,1850,1852,1855,1857,1859,1861,1863,1866,1868],{"class":244,"line":392},[242,1837,958],{"class":252},[242,1839,1840],{"class":301}," session",[242,1842,596],{"class":252},[242,1844,1845],{"class":301}," ZIO",[242,1847,602],{"class":248},[242,1849,731],{"class":301},[242,1851,195],{"class":248},[242,1853,1854],{"class":301},"Throwable",[242,1856,195],{"class":248},[242,1858,1819],{"class":301},[242,1860,602],{"class":248},[242,1862,1814],{"class":301},[242,1864,1865],{"class":248},"]] ",[242,1867,611],{"class":252},[242,1869,1870],{"class":248}," sessions.toScopedZIO\n",[242,1872,1873],{"class":244,"line":568},[242,1874,338],{"emptyLinePlaceholder":337},[242,1876,1877,1879,1881],{"class":244,"line":580},[242,1878,571],{"class":252},[242,1880,1799],{"class":301},[242,1882,577],{"class":252},[242,1884,1885],{"class":244,"line":587},[242,1886,1887],{"class":583},"    \u002F** Reads the startup runtime config and creates the pool. Connections open on first use. *\u002F\n",[242,1889,1890,1892,1894,1896,1899,1901,1904,1906,1908,1910,1913,1915],{"class":244,"line":619},[242,1891,590],{"class":252},[242,1893,1711],{"class":359},[242,1895,596],{"class":252},[242,1897,1898],{"class":301}," ZLayer",[242,1900,602],{"class":248},[242,1902,1903],{"class":301},"Any",[242,1905,195],{"class":248},[242,1907,1854],{"class":301},[242,1909,195],{"class":248},[242,1911,1912],{"class":301},"SessionPool",[242,1914,608],{"class":248},[242,1916,1147],{"class":252},[242,1918,1919,1922],{"class":244,"line":637},[242,1920,1921],{"class":301},"      ZLayer",[242,1923,1924],{"class":248},".scoped(\n",[242,1926,1927],{"class":244,"line":649},[242,1928,1929],{"class":252},"        for\n",[242,1931,1932,1935,1938,1941,1943,1945],{"class":244,"line":661},[242,1933,1934],{"class":248},"          config   ",[242,1936,1937],{"class":252},"\u003C-",[242,1939,1940],{"class":301}," ZRuntimeConfig",[242,1942,625],{"class":248},[242,1944,628],{"class":301},[242,1946,1947],{"class":248},"]\n",[242,1949,1950,1953,1955],{"class":244,"line":692},[242,1951,1952],{"class":248},"          sessions ",[242,1954,1937],{"class":252},[242,1956,1957],{"class":248}," pooled(config.database).toScopedZIO\n",[242,1959,1960,1963,1965],{"class":244,"line":704},[242,1961,1962],{"class":252},"        yield",[242,1964,1799],{"class":301},[242,1966,1967],{"class":248},"(sessions),\n",[242,1969,1970],{"class":244,"line":713},[242,1971,1972],{"class":248},"      )\n",[242,1974,1975],{"class":244,"line":719},[242,1976,338],{"emptyLinePlaceholder":337},[242,1978,1979,1981,1983,1986,1988,1991,1993,1995,1997,1999,2002,2004,2006,2008,2010,2012,2014,2016,2018,2020,2022,2025],{"class":244,"line":1092},[242,1980,1337],{"class":252},[242,1982,1037],{"class":252},[242,1984,1985],{"class":301}," pooled",[242,1987,356],{"class":248},[242,1989,1990],{"class":359},"config",[242,1992,363],{"class":248},[242,1994,411],{"class":301},[242,1996,973],{"class":248},[242,1998,596],{"class":252},[242,2000,2001],{"class":301}," Resource",[242,2003,602],{"class":248},[242,2005,1814],{"class":301},[242,2007,195],{"class":248},[242,2009,1809],{"class":301},[242,2011,602],{"class":248},[242,2013,1814],{"class":301},[242,2015,195],{"class":248},[242,2017,1819],{"class":301},[242,2019,602],{"class":248},[242,2021,1814],{"class":301},[242,2023,2024],{"class":248},"]]] ",[242,2026,1147],{"class":252},[242,2028,2029,2032,2035,2037,2039,2042,2044,2046],{"class":244,"line":1097},[242,2030,2031],{"class":252},"      given",[242,2033,2034],{"class":301}," Tracer",[242,2036,602],{"class":248},[242,2038,1814],{"class":301},[242,2040,2041],{"class":248},"]  ",[242,2043,611],{"class":252},[242,2045,2034],{"class":301},[242,2047,2048],{"class":248},".noop\n",[242,2050,2051,2053,2056,2058,2060,2062,2064,2066,2069,2071],{"class":244,"line":1150},[242,2052,2031],{"class":252},[242,2054,2055],{"class":301}," Console",[242,2057,602],{"class":248},[242,2059,1814],{"class":301},[242,2061,608],{"class":248},[242,2063,611],{"class":252},[242,2065,2055],{"class":301},[242,2067,2068],{"class":248},".make[",[242,2070,1814],{"class":301},[242,2072,1947],{"class":248},[242,2074,2075,2077,2080,2082,2084,2086,2088,2090,2093,2095],{"class":244,"line":1167},[242,2076,2031],{"class":252},[242,2078,2079],{"class":301}," Network",[242,2081,602],{"class":248},[242,2083,1814],{"class":301},[242,2085,608],{"class":248},[242,2087,611],{"class":252},[242,2089,2079],{"class":301},[242,2091,2092],{"class":248},".forAsync[",[242,2094,1814],{"class":301},[242,2096,1947],{"class":248},[242,2098,2099,2102,2105,2107,2110],{"class":244,"line":1203},[242,2100,2101],{"class":252},"      val",[242,2103,2104],{"class":359}," password",[242,2106,980],{"class":252},[242,2108,2109],{"class":301}," Some",[242,2111,2112],{"class":248},"(config.password.reveal).filter(_.nonEmpty)\n",[242,2114,2115,2118,2120,2123,2125,2127],{"class":244,"line":1220},[242,2116,2117],{"class":301},"      Session",[242,2119,167],{"class":248},[242,2121,2122],{"class":301},"Builder",[242,2124,602],{"class":248},[242,2126,1814],{"class":301},[242,2128,1947],{"class":248},[242,2130,2131],{"class":244,"line":1244},[242,2132,2133],{"class":248},"        .withHost(config.host)\n",[242,2135,2136],{"class":244,"line":1250},[242,2137,2138],{"class":248},"        .withPort(config.port)\n",[242,2140,2141,2144,2146,2148,2151],{"class":244,"line":1255},[242,2142,2143],{"class":248},"        .withCredentials(",[242,2145,1819],{"class":301},[242,2147,167],{"class":248},[242,2149,2150],{"class":301},"Credentials",[242,2152,2153],{"class":248},"(config.user, password))\n",[242,2155,2156],{"class":244,"line":1294},[242,2157,2158],{"class":248},"        .withDatabase(config.name)\n",[242,2160,2161,2164,2166,2169],{"class":244,"line":1314},[242,2162,2163],{"class":248},"        .pooled(max ",[242,2165,611],{"class":252},[242,2167,2168],{"class":1050}," 4",[242,2170,895],{"class":248},[99,2172,2173,2175,2176,2178,2179,2182,2183,167],{},[110,2174,223],{}," wraps the layer in ",[110,2177,116],{},", so all requests share one pool: ",[110,2180,2181],{},"ZShared.layer(SessionPool.layer)",". See ",[125,2184,27],{"href":2185},"\u002Fguide\u002Fzio#compose-services-in-your-apphandler",[274,2187,2189],{"id":2188},"take-one-session-per-request","Take one session per request",[99,2191,2192,2195,2196,2198,2199,2201,2202,596],{},[110,2193,2194],{},"NotesLive.layer"," takes at most one session per request, in the request scope, on first use. The session returns to the pool when the request ends, and a request that fails validation never takes one: invalid input is a 422 even when the database is down. A private ",[110,2197,1238],{}," is the table row; the service returns the domain ",[110,2200,198],{},", and the handlers send ",[110,2203,201],{},[233,2205,2207],{"className":235,"code":2206,"language":237,"meta":238,"style":238},"\u002F** Notes in PostgreSQL. `session` gives the request's session; the first call takes it. *\u002F\nfinal class NotesLive(session: Task[Session[Task]]) extends Notes:\n    import NotesLive.{insert, selectAll}\n\n    def create(value: String): Task[Note] = session.flatMap(_.unique(insert)(value)).map(_.toNote)\n\n    def list: Task[List[Note]] = session.flatMap(_.execute(selectAll)).map(_.map(_.toNote))\n\nobject NotesLive:\n    \u002F** The table row. It stays inside this interpreter: a new column does not reach the JSON. *\u002F\n    private final case class NoteRow(id: Int, text: String):\n        def toNote: Note = Note(id, text)\n\n    private val row = (int4 *: text).to[NoteRow]\n\n    private val insert: Query[String, NoteRow] = sql\"INSERT INTO h34s_notes (text) VALUES ($text) RETURNING id, text\".query(row)\n\n    private val selectAll: Query[skunk.Void, NoteRow] = sql\"SELECT id, text FROM h34s_notes ORDER BY id\".query(row)\n\n    val layer: ZLayer[SessionPool & Scope, Nothing, Notes] =\n      ZLayer.fromZIO(\n        for\n          pool    \u003C- ZIO.service[SessionPool]\n          scope   \u003C- ZIO.service[Scope]\n          session \u003C- pool.session.provideEnvironment(ZEnvironment(scope)).memoize\n        yield NotesLive(session),\n      )\n",[110,2208,2209,2214,2249,2267,2271,2304,2308,2334,2338,2346,2351,2383,2399,2403,2428,2432,2471,2475,2509,2513,2546,2553,2557,2573,2588,2604,2613],{"__ignoreMap":238},[242,2210,2211],{"class":244,"line":245},[242,2212,2213],{"class":583},"\u002F** Notes in PostgreSQL. `session` gives the request's session; the first call takes it. *\u002F\n",[242,2215,2216,2218,2220,2222,2224,2227,2229,2231,2233,2235,2237,2239,2242,2244,2247],{"class":244,"line":334},[242,2217,344],{"class":252},[242,2219,350],{"class":252},[242,2221,1064],{"class":301},[242,2223,356],{"class":248},[242,2225,2226],{"class":359},"session",[242,2228,363],{"class":248},[242,2230,1814],{"class":301},[242,2232,602],{"class":248},[242,2234,1819],{"class":301},[242,2236,602],{"class":248},[242,2238,1814],{"class":301},[242,2240,2241],{"class":248},"]]) ",[242,2243,1006],{"class":252},[242,2245,2246],{"class":301}," Notes",[242,2248,577],{"class":252},[242,2250,2251,2253,2255,2257,2260,2262,2265],{"class":244,"line":341},[242,2252,1082],{"class":252},[242,2254,1064],{"class":301},[242,2256,320],{"class":248},[242,2258,2259],{"class":301},"insert",[242,2261,195],{"class":248},[242,2263,2264],{"class":301},"selectAll",[242,2266,331],{"class":248},[242,2268,2269],{"class":244,"line":392},[242,2270,338],{"emptyLinePlaceholder":337},[242,2272,2273,2275,2277,2279,2282,2284,2286,2288,2290,2293,2295,2297,2299,2301],{"class":244,"line":568},[242,2274,958],{"class":252},[242,2276,1102],{"class":301},[242,2278,356],{"class":248},[242,2280,2281],{"class":359},"value",[242,2283,363],{"class":248},[242,2285,892],{"class":301},[242,2287,973],{"class":248},[242,2289,596],{"class":252},[242,2291,2292],{"class":301}," Task",[242,2294,602],{"class":248},[242,2296,198],{"class":301},[242,2298,608],{"class":248},[242,2300,611],{"class":252},[242,2302,2303],{"class":248}," session.flatMap(_.unique(insert)(value)).map(_.toNote)\n",[242,2305,2306],{"class":244,"line":580},[242,2307,338],{"emptyLinePlaceholder":337},[242,2309,2310,2312,2314,2316,2318,2320,2323,2325,2327,2329,2331],{"class":244,"line":587},[242,2311,958],{"class":252},[242,2313,1260],{"class":301},[242,2315,596],{"class":252},[242,2317,2292],{"class":301},[242,2319,602],{"class":248},[242,2321,2322],{"class":301},"List",[242,2324,602],{"class":248},[242,2326,198],{"class":301},[242,2328,1865],{"class":248},[242,2330,611],{"class":252},[242,2332,2333],{"class":248}," session.flatMap(_.execute(selectAll)).map(_.map(_.toNote))\n",[242,2335,2336],{"class":244,"line":619},[242,2337,338],{"emptyLinePlaceholder":337},[242,2339,2340,2342,2344],{"class":244,"line":637},[242,2341,571],{"class":252},[242,2343,1064],{"class":301},[242,2345,577],{"class":252},[242,2347,2348],{"class":244,"line":649},[242,2349,2350],{"class":583},"    \u002F** The table row. It stays inside this interpreter: a new column does not reach the JSON. *\u002F\n",[242,2352,2353,2355,2357,2359,2361,2363,2365,2367,2369,2371,2373,2375,2377,2379,2381],{"class":244,"line":661},[242,2354,1337],{"class":252},[242,2356,1340],{"class":252},[242,2358,347],{"class":252},[242,2360,350],{"class":252},[242,2362,1347],{"class":301},[242,2364,356],{"class":248},[242,2366,877],{"class":359},[242,2368,363],{"class":248},[242,2370,882],{"class":301},[242,2372,195],{"class":248},[242,2374,887],{"class":359},[242,2376,363],{"class":248},[242,2378,892],{"class":301},[242,2380,973],{"class":248},[242,2382,577],{"class":252},[242,2384,2385,2387,2389,2391,2393,2395,2397],{"class":244,"line":692},[242,2386,1377],{"class":252},[242,2388,1380],{"class":301},[242,2390,596],{"class":252},[242,2392,872],{"class":301},[242,2394,980],{"class":252},[242,2396,872],{"class":301},[242,2398,1391],{"class":248},[242,2400,2401],{"class":244,"line":704},[242,2402,338],{"emptyLinePlaceholder":337},[242,2404,2405,2407,2410,2413,2415,2418,2421,2424,2426],{"class":244,"line":713},[242,2406,1337],{"class":252},[242,2408,2409],{"class":252}," val",[242,2411,2412],{"class":359}," row",[242,2414,980],{"class":252},[242,2416,2417],{"class":248}," (int4 ",[242,2419,2420],{"class":252},"*:",[242,2422,2423],{"class":248}," text).to[",[242,2425,1238],{"class":301},[242,2427,1947],{"class":248},[242,2429,2430],{"class":244,"line":719},[242,2431,338],{"emptyLinePlaceholder":337},[242,2433,2434,2436,2438,2441,2443,2446,2448,2450,2452,2454,2456,2458,2461,2464,2466,2468],{"class":244,"line":1092},[242,2435,1337],{"class":252},[242,2437,2409],{"class":252},[242,2439,2440],{"class":359}," insert",[242,2442,596],{"class":252},[242,2444,2445],{"class":301}," Query",[242,2447,602],{"class":248},[242,2449,892],{"class":301},[242,2451,195],{"class":248},[242,2453,1238],{"class":301},[242,2455,608],{"class":248},[242,2457,611],{"class":252},[242,2459,2460],{"class":252}," sql",[242,2462,2463],{"class":256},"\"INSERT INTO h34s_notes (text) VALUES (",[242,2465,1229],{"class":248},[242,2467,1232],{"class":256},[242,2469,2470],{"class":248},".query(row)\n",[242,2472,2473],{"class":244,"line":1097},[242,2474,338],{"emptyLinePlaceholder":337},[242,2476,2477,2479,2481,2484,2486,2488,2491,2494,2496,2498,2500,2502,2504,2507],{"class":244,"line":1150},[242,2478,1337],{"class":252},[242,2480,2409],{"class":252},[242,2482,2483],{"class":359}," selectAll",[242,2485,596],{"class":252},[242,2487,2445],{"class":301},[242,2489,2490],{"class":248},"[skunk.",[242,2492,2493],{"class":301},"Void",[242,2495,195],{"class":248},[242,2497,1238],{"class":301},[242,2499,608],{"class":248},[242,2501,611],{"class":252},[242,2503,2460],{"class":252},[242,2505,2506],{"class":256},"\"SELECT id, text FROM h34s_notes ORDER BY id\"",[242,2508,2470],{"class":248},[242,2510,2511],{"class":244,"line":1167},[242,2512,338],{"emptyLinePlaceholder":337},[242,2514,2515,2517,2519,2521,2523,2525,2527,2529,2532,2534,2537,2539,2542,2544],{"class":244,"line":1203},[242,2516,590],{"class":252},[242,2518,1711],{"class":359},[242,2520,596],{"class":252},[242,2522,1898],{"class":301},[242,2524,602],{"class":248},[242,2526,1912],{"class":301},[242,2528,1128],{"class":252},[242,2530,2531],{"class":301}," Scope",[242,2533,195],{"class":248},[242,2535,2536],{"class":301},"Nothing",[242,2538,195],{"class":248},[242,2540,2541],{"class":301},"Notes",[242,2543,608],{"class":248},[242,2545,1147],{"class":252},[242,2547,2548,2550],{"class":244,"line":1220},[242,2549,1921],{"class":301},[242,2551,2552],{"class":248},".fromZIO(\n",[242,2554,2555],{"class":244,"line":1244},[242,2556,1929],{"class":252},[242,2558,2559,2562,2564,2566,2569,2571],{"class":244,"line":1250},[242,2560,2561],{"class":248},"          pool    ",[242,2563,1937],{"class":252},[242,2565,1845],{"class":301},[242,2567,2568],{"class":248},".service[",[242,2570,1912],{"class":301},[242,2572,1947],{"class":248},[242,2574,2575,2578,2580,2582,2584,2586],{"class":244,"line":1255},[242,2576,2577],{"class":248},"          scope   ",[242,2579,1937],{"class":252},[242,2581,1845],{"class":301},[242,2583,2568],{"class":248},[242,2585,731],{"class":301},[242,2587,1947],{"class":248},[242,2589,2590,2593,2595,2598,2601],{"class":244,"line":1294},[242,2591,2592],{"class":248},"          session ",[242,2594,1937],{"class":252},[242,2596,2597],{"class":248}," pool.session.provideEnvironment(",[242,2599,2600],{"class":301},"ZEnvironment",[242,2602,2603],{"class":248},"(scope)).memoize\n",[242,2605,2606,2608,2610],{"class":244,"line":1314},[242,2607,1962],{"class":252},[242,2609,1064],{"class":301},[242,2611,2612],{"class":248},"(session),\n",[242,2614,2615],{"class":244,"line":1319},[242,2616,1972],{"class":248},[99,2618,2619,2622],{},[110,2620,2621],{},"NotesHandlerSpec"," checks the order with a pool whose database is down: invalid input takes no session; valid input takes one.",[99,2624,2625,2626,2628,2629,2632,2633,2636,2637,2640,2641,2643,2644,2647,2648,2651],{},"Skunk's ",[110,2627,1229],{}," is a parameter codec; the value is bound separately. For a transaction, open Skunk's ",[110,2630,2631],{},"session.transaction"," resource (",[110,2634,2635],{},"Resource[Task, Transaction[Task]]",") in the same request scope, for example with ",[110,2638,2639],{},".toScopedZIO",". To map a unique violation to your own ",[110,2642,176],{},", match ",[110,2645,2646],{},"case SqlState.UniqueViolation(_) =>"," on the failure inside the service (",[110,2649,2650],{},"skunk.SqlState",", SQLSTATE 23505). These two recipes come from the Skunk API; the demo does not use them.",[274,2653,2655],{"id":2654},"keep-the-skunk-node-plugin","Keep the Skunk Node plugin",[99,2657,2658,2659,2662,2663,1430,2666,2667,2669,2670,2673],{},"Skunk 1.0.0-M12 calls ",[110,2660,2661],{},"require('crypto')"," for SCRAM authentication, even in ES-module output. The demo installs Node's loader in a Nitro plugin, ",[110,2664,2665],{},"examples\u002Fnuxt\u002Fserver\u002Fplugins\u002Fskunk-node-crypto.ts"," With that plugin, the production bundle logged in to PostgreSQL 16 with ",[110,2668,1717],{},"; PostgreSQL's log showed ",[110,2671,2672],{},"connection authenticated: identity=\"h34s\" method=scram-sha-256",". Copy that plugin when you use Skunk in your own Nuxt app. If authentication ran before the plugin was installed, restart the server.",[134,2675,2677],{"id":2676},"set-up-a-local-database","Set up a local database",[99,2679,2680,2681,2685,2686,2689,2690,2693],{},"The demo's PostgreSQL setup, initialization script and smoke test are in the ",[125,2682,1739],{"href":2683,"rel":2684},"https:\u002F\u002Fgithub.com\u002Fyuri4n\u002Fh34s\u002Fblob\u002Fmain\u002Fdocs\u002Fdevelopment.md",[1664],". To use an installed PostgreSQL without Docker, as in a Claude Code cloud session, follow ",[125,2687,1665],{"href":1662,"rel":2688},[1664],". The demo's table is created by ",[110,2691,2692],{},"examples\u002Fnuxt\u002Fscala\u002Fzio\u002Fsrc\u002Fmain\u002Fresources\u002Fsql\u002F01-init.sql",", which PostgreSQL runs only for an empty volume.",[134,2695,2697],{"id":2696},"test-without-a-database","Test without a database",[99,2699,2700,2701,2703,2704,2706,2707,2709,2710,2713],{},"Keep the service behind a trait, and test handlers with an in-memory implementation. The demo's ",[110,2702,2621],{}," runs the ZIO handlers with a fake request and an in-memory ",[110,2705,2541],{},", and ",[110,2708,194],{}," with a pool whose database is down; see ",[125,2711,2712],{"href":52},"testing",". Test the SQL itself against a real PostgreSQL in your own integration tests.",[2715,2716,2717],"style",{},"html pre.shiki code .sxrX7, html code.shiki .sxrX7{--shiki-light:#24292E;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s8jYJ, html code.shiki .s8jYJ{--shiki-light:#D73A49;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIIMD, html code.shiki .sIIMD{--shiki-light:#032F62;--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .snPdu, html code.shiki .snPdu{--shiki-light:#6F42C1;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sP4rz, html code.shiki .sP4rz{--shiki-light:#E36209;--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sCsY4, html code.shiki .sCsY4{--shiki-light:#6A737D;--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sBjJW, html code.shiki .sBjJW{--shiki-light:#005CC5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":238,"searchDepth":334,"depth":334,"links":2719},[2720,2721,2729,2736,2737],{"id":136,"depth":334,"text":137},{"id":183,"depth":334,"text":184,"children":2722},[2723,2724,2725,2726,2727,2728],{"id":276,"depth":341,"text":277},{"id":504,"depth":341,"text":505},{"id":752,"depth":341,"text":753},{"id":847,"depth":341,"text":848},{"id":1583,"depth":341,"text":1584},{"id":1630,"depth":341,"text":216},{"id":1707,"depth":334,"text":2730,"children":2731},"ZIO: Skunk in a ZShared layer",[2732,2733,2734,2735],{"id":1743,"depth":341,"text":1744},{"id":1773,"depth":341,"text":1774},{"id":2188,"depth":341,"text":2189},{"id":2654,"depth":341,"text":2655},{"id":2676,"depth":334,"text":2677},{"id":2696,"depth":334,"text":2697},"Use kyo-sql or Skunk directly inside Nitro, with one pool per application and one transaction or session per request.","md",null,{},{"title":43,"description":2738},"JZnvMzNGm0G-wQFFnN6WEC7xoZHLL8VAn_c1U7rh554",[2745,2747],{"title":39,"path":40,"stem":41,"description":2746,"children":-1},"Derive the native value, JSON, YAML and validation from one schema per model, with Kyo Schema or ZIO Schema.",{"title":47,"path":48,"stem":49,"description":2748,"children":-1},"Send a Kyo Stream or a ZStream with Response.sendStream; h34s writes the head at once, keeps the request scope open and waits for cleanup when the client goes away.",1792533381917]