[{"data":1,"prerenderedAt":1578},["ShallowReactive",2],{"navigation_docs":3,"-playbooks-local-postgrest":80,"-playbooks-local-postgrest-surround":1573},[4,20,45],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":6},"Getting Started",false,"\u002Fgetting-started","1.getting-started",[10,15],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fgetting-started\u002Fintroduction","1.getting-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Installation","\u002Fgetting-started\u002Finstallation","1.getting-started\u002F2.installation","i-lucide-download",{"title":21,"icon":6,"path":22,"stem":23,"children":24,"page":6},"Guide","\u002Fguide","2.guide",[25,30,35,40],{"title":26,"path":27,"stem":28,"icon":29},"Configuration","\u002Fguide\u002Fconfiguration","2.guide\u002F1.configuration","i-lucide-settings",{"title":31,"path":32,"stem":33,"icon":34},"Composables","\u002Fguide\u002Fcomposables","2.guide\u002F2.composables","i-lucide-code",{"title":36,"path":37,"stem":38,"icon":39},"Authentication","\u002Fguide\u002Fauthentication","2.guide\u002F3.authentication","i-lucide-key-round",{"title":41,"path":42,"stem":43,"icon":44},"Types","\u002Fguide\u002Ftypes","2.guide\u002F4.types","i-lucide-braces",{"title":46,"icon":6,"path":47,"stem":48,"children":49,"page":6},"Playbooks","\u002Fplaybooks","3.playbooks",[50,55,60,65,70,75],{"title":51,"path":52,"stem":53,"icon":54},"Local Postgres & PostgREST","\u002Fplaybooks\u002Flocal-postgrest","3.playbooks\u002F1.local-postgrest","i-simple-icons-docker",{"title":56,"path":57,"stem":58,"icon":59},"Coming from Supabase","\u002Fplaybooks\u002Fcoming-from-supabase","3.playbooks\u002F2.coming-from-supabase","i-lucide-arrow-right-left",{"title":61,"path":62,"stem":63,"icon":64},"Auth with nuxt-auth-utils","\u002Fplaybooks\u002Fauth-nuxt-auth-utils","3.playbooks\u002F3.auth-nuxt-auth-utils","i-lucide-key",{"title":66,"path":67,"stem":68,"icon":69},"Auth with Better Auth","\u002Fplaybooks\u002Fauth-better-auth","3.playbooks\u002F4.auth-better-auth","i-lucide-shield",{"title":71,"path":72,"stem":73,"icon":74},"Auth with Sidebase Nuxt Auth","\u002Fplaybooks\u002Fauth-sidebase","3.playbooks\u002F5.auth-sidebase","i-lucide-user-round-check",{"title":76,"path":77,"stem":78,"icon":79},"Auth with Clerk","\u002Fplaybooks\u002Fauth-clerk","3.playbooks\u002F6.auth-clerk","i-lucide-circle-user-round",{"id":81,"title":51,"body":82,"description":1566,"extension":1567,"links":1568,"meta":1569,"navigation":1570,"path":52,"seo":1571,"stem":53,"__hash__":1572},"docs\u002F3.playbooks\u002F1.local-postgrest.md",{"type":83,"value":84,"toc":1552},"minimark",[85,94,105,110,140,147,151,166,189,192,904,908,915,918,1009,1012,1187,1190,1220,1223,1248,1255,1259,1277,1284,1329,1350,1354,1358,1457,1461,1509,1531,1548],[86,87,88,89,93],"p",{},"This page assumes nothing: not that you have Postgres installed, not that you've used PostgREST before, not that Docker is already running. By the end you'll have a real database and a real PostgREST API on your machine, and you'll have queried it with ",[90,91,92],"code",{},"curl"," before ever touching Nuxt.",[86,95,96,100,101,104],{},[97,98,99],"strong",{},"The opinionated take:"," don't install Postgres natively, and don't install PostgREST as a binary. Run both in Docker. It's disposable: if you break it, ",[90,102,103],{},"docker compose down -v"," wipes it and you start clean in seconds. This is also exactly what this module's own tests and playground do, so it's a setup that's continuously verified to work.",[106,107,109],"h2",{"id":108},"what-these-two-things-actually-are","What these two things actually are",[111,112,113,120],"ul",{},[114,115,116,119],"li",{},[97,117,118],{},"Postgres"," is the database. It stores your data in tables and enforces rules about who can read or write which rows (that last part, row-level security, is what makes this whole stack interesting for auth).",[114,121,122,131,132,135,136,139],{},[97,123,124],{},[125,126,130],"a",{"href":127,"rel":128},"https:\u002F\u002Fpostgrest.org",[129],"nofollow","PostgREST"," is a standalone web server that sits in front of Postgres and turns your database schema into a REST API automatically. You don't write API endpoints. PostgREST inspects your tables and generates ",[90,133,134],{},"GET \u002Ftodos",", ",[90,137,138],{},"POST \u002Ftodos",", filtering, pagination, etc. for you. Your app never connects to Postgres directly; it talks to PostgREST over HTTP.",[86,141,142,143,146],{},"They're kept as ",[97,144,145],{},"two separate Compose files"," below, on purpose: one Postgres instance is meant to back any number of databases, and each database gets its own PostgREST container. That's not a hypothetical, it's the second half of this guide.",[106,148,150],{"id":149},"prerequisites","Prerequisites",[86,152,153,154,159,160,165],{},"Install ",[125,155,158],{"href":156,"rel":157},"https:\u002F\u002Fwww.docker.com\u002Fproducts\u002Fdocker-desktop\u002F",[129],"Docker Desktop"," (or ",[125,161,164],{"href":162,"rel":163},"https:\u002F\u002Forbstack.dev\u002F",[129],"OrbStack"," on macOS) and make sure it's actually running:",[167,168,174],"pre",{"className":169,"code":170,"filename":171,"language":172,"meta":173,"style":173},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","docker --version\n","Terminal","bash","",[90,175,176],{"__ignoreMap":173},[177,178,181,185],"span",{"class":179,"line":180},"line",1,[177,182,184],{"class":183},"sBMFI","docker",[177,186,188],{"class":187},"sfazB"," --version\n",[86,190,191],{},"If that prints a version instead of \"command not found,\" you're set.",[193,194,195,200,203,360,370,468,483,487,490,702,706,738,757,761,798,805,809,821,824,836,882],"steps",{},[196,197,199],"h3",{"id":198},"create-the-postgres-compose-file","Create the Postgres Compose file",[86,201,202],{},"Make a project folder with these two files. This one is the shared, long-lived instance, so you won't touch it again when you add more databases later.",[167,204,209],{"className":205,"code":206,"filename":207,"language":208,"meta":173,"style":173},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","services:\n  db:\n    image: postgres:18\n    environment:\n      POSTGRES_PASSWORD: postgres\n    ports: ['5432:5432']\n    volumes:\n      - .\u002Finit.sql:\u002Fdocker-entrypoint-initdb.d\u002Finit.sql:ro\n    healthcheck:\n      test: ['CMD-SHELL', 'pg_isready -U postgres']\n      interval: 2s\n      retries: 20\n","docker-compose.yml","yaml",[90,210,211,221,229,241,249,260,282,290,299,307,337,348],{"__ignoreMap":173},[177,212,213,217],{"class":179,"line":180},[177,214,216],{"class":215},"swJcz","services",[177,218,220],{"class":219},"sMK4o",":\n",[177,222,224,227],{"class":179,"line":223},2,[177,225,226],{"class":215},"  db",[177,228,220],{"class":219},[177,230,232,235,238],{"class":179,"line":231},3,[177,233,234],{"class":215},"    image",[177,236,237],{"class":219},":",[177,239,240],{"class":187}," postgres:18\n",[177,242,244,247],{"class":179,"line":243},4,[177,245,246],{"class":215},"    environment",[177,248,220],{"class":219},[177,250,252,255,257],{"class":179,"line":251},5,[177,253,254],{"class":215},"      POSTGRES_PASSWORD",[177,256,237],{"class":219},[177,258,259],{"class":187}," postgres\n",[177,261,263,266,268,271,274,277,279],{"class":179,"line":262},6,[177,264,265],{"class":215},"    ports",[177,267,237],{"class":219},[177,269,270],{"class":219}," [",[177,272,273],{"class":219},"'",[177,275,276],{"class":187},"5432:5432",[177,278,273],{"class":219},[177,280,281],{"class":219},"]\n",[177,283,285,288],{"class":179,"line":284},7,[177,286,287],{"class":215},"    volumes",[177,289,220],{"class":219},[177,291,293,296],{"class":179,"line":292},8,[177,294,295],{"class":219},"      -",[177,297,298],{"class":187}," .\u002Finit.sql:\u002Fdocker-entrypoint-initdb.d\u002Finit.sql:ro\n",[177,300,302,305],{"class":179,"line":301},9,[177,303,304],{"class":215},"    healthcheck",[177,306,220],{"class":219},[177,308,310,313,315,317,319,322,324,327,330,333,335],{"class":179,"line":309},10,[177,311,312],{"class":215},"      test",[177,314,237],{"class":219},[177,316,270],{"class":219},[177,318,273],{"class":219},[177,320,321],{"class":187},"CMD-SHELL",[177,323,273],{"class":219},[177,325,326],{"class":219},",",[177,328,329],{"class":219}," '",[177,331,332],{"class":187},"pg_isready -U postgres",[177,334,273],{"class":219},[177,336,281],{"class":219},[177,338,340,343,345],{"class":179,"line":339},11,[177,341,342],{"class":215},"      interval",[177,344,237],{"class":219},[177,346,347],{"class":187}," 2s\n",[177,349,351,354,356],{"class":179,"line":350},12,[177,352,353],{"class":215},"      retries",[177,355,237],{"class":219},[177,357,359],{"class":358},"sbssI"," 20\n",[86,361,362,365,366,369],{},[90,363,364],{},"init.sql"," runs once, the first time the ",[90,367,368],{},"db"," volume is created. It's where the roles PostgREST needs come from, plus a table to try it out on:",[167,371,375],{"className":372,"code":373,"filename":364,"language":374,"meta":173,"style":173},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","-- Roles PostgREST switches between per-request, based on the JWT it's given.\n-- Roles are cluster-wide in Postgres, not per-database, so this file only\n-- needs to run once, ever. Every database you add later reuses these.\ncreate role anon nologin;                              -- no token \u002F logged out\ncreate role authenticated nologin;                      -- valid user JWT\ncreate role service_role nologin bypassrls;              -- your backend, bypasses RLS\ncreate role authenticator noinherit login password 'authenticator'; -- who PGRST_DB_URI connects as\ngrant anon, authenticated, service_role to authenticator;\ngrant usage on schema public to anon, authenticated, service_role;\n\n-- A table to actually query\ncreate table todos (\n  id bigint generated always as identity primary key,\n  title text not null\n);\ngrant select, insert on todos to anon;\ninsert into todos (title) values ('Buy milk');\n","sql",[90,376,377,382,387,392,397,402,407,412,417,422,428,433,438,444,450,456,462],{"__ignoreMap":173},[177,378,379],{"class":179,"line":180},[177,380,381],{},"-- Roles PostgREST switches between per-request, based on the JWT it's given.\n",[177,383,384],{"class":179,"line":223},[177,385,386],{},"-- Roles are cluster-wide in Postgres, not per-database, so this file only\n",[177,388,389],{"class":179,"line":231},[177,390,391],{},"-- needs to run once, ever. Every database you add later reuses these.\n",[177,393,394],{"class":179,"line":243},[177,395,396],{},"create role anon nologin;                              -- no token \u002F logged out\n",[177,398,399],{"class":179,"line":251},[177,400,401],{},"create role authenticated nologin;                      -- valid user JWT\n",[177,403,404],{"class":179,"line":262},[177,405,406],{},"create role service_role nologin bypassrls;              -- your backend, bypasses RLS\n",[177,408,409],{"class":179,"line":284},[177,410,411],{},"create role authenticator noinherit login password 'authenticator'; -- who PGRST_DB_URI connects as\n",[177,413,414],{"class":179,"line":292},[177,415,416],{},"grant anon, authenticated, service_role to authenticator;\n",[177,418,419],{"class":179,"line":301},[177,420,421],{},"grant usage on schema public to anon, authenticated, service_role;\n",[177,423,424],{"class":179,"line":309},[177,425,427],{"emptyLinePlaceholder":426},true,"\n",[177,429,430],{"class":179,"line":339},[177,431,432],{},"-- A table to actually query\n",[177,434,435],{"class":179,"line":350},[177,436,437],{},"create table todos (\n",[177,439,441],{"class":179,"line":440},13,[177,442,443],{},"  id bigint generated always as identity primary key,\n",[177,445,447],{"class":179,"line":446},14,[177,448,449],{},"  title text not null\n",[177,451,453],{"class":179,"line":452},15,[177,454,455],{},");\n",[177,457,459],{"class":179,"line":458},16,[177,460,461],{},"grant select, insert on todos to anon;\n",[177,463,465],{"class":179,"line":464},17,[177,466,467],{},"insert into todos (title) values ('Buy milk');\n",[86,469,470,473,474,478,479,482],{},[90,471,472],{},"authenticator"," is a technical detail, not a \"real\" user: it's the only role PostgREST itself logs into Postgres as, and it can only ever act ",[475,476,477],"em",{},"as"," one of the three roles above via ",[90,480,481],{},"set role",", on Postgres's authority, never a role of PostgREST's own choosing.",[196,484,486],{"id":485},"create-the-postgrest-compose-file","Create the PostgREST Compose file",[86,488,489],{},"This is the piece that's per-database. Keep it in a separate file from Postgres. You'll add another one just like it for every database you want exposed.",[167,491,494],{"className":205,"code":492,"filename":493,"language":208,"meta":173,"style":173},"services:\n  postgrest:\n    image: postgrest\u002Fpostgrest:v16.3\n    depends_on:\n      db:\n        condition: service_healthy\n    environment:\n      PGRST_DB_URI: postgres:\u002F\u002Fauthenticator:authenticator@db:5432\u002Fpostgres\n      PGRST_DB_SCHEMAS: public\n      PGRST_DB_ANON_ROLE: anon\n      PGRST_JWT_SECRET: replace-with-a-secret-of-at-least-32-characters\n      # Needed for the healthcheck below: --ready checks the admin server, which only\n      # starts if given a port, and only accepts --ready's probe on a literal host\n      # (not the default \"all interfaces\"). It isn't published, so this is internal-only.\n      PGRST_ADMIN_SERVER_PORT: 3002\n      PGRST_ADMIN_SERVER_HOST: localhost\n    ports: ['3001:3000']\n    healthcheck:\n      test: ['CMD', 'postgrest', '--ready']\n      interval: 2s\n      retries: 20\n","docker-compose.postgrest.yml",[90,495,496,502,509,518,525,532,542,548,558,568,578,588,594,599,604,614,624,641,648,684,693],{"__ignoreMap":173},[177,497,498,500],{"class":179,"line":180},[177,499,216],{"class":215},[177,501,220],{"class":219},[177,503,504,507],{"class":179,"line":223},[177,505,506],{"class":215},"  postgrest",[177,508,220],{"class":219},[177,510,511,513,515],{"class":179,"line":231},[177,512,234],{"class":215},[177,514,237],{"class":219},[177,516,517],{"class":187}," postgrest\u002Fpostgrest:v16.3\n",[177,519,520,523],{"class":179,"line":243},[177,521,522],{"class":215},"    depends_on",[177,524,220],{"class":219},[177,526,527,530],{"class":179,"line":251},[177,528,529],{"class":215},"      db",[177,531,220],{"class":219},[177,533,534,537,539],{"class":179,"line":262},[177,535,536],{"class":215},"        condition",[177,538,237],{"class":219},[177,540,541],{"class":187}," service_healthy\n",[177,543,544,546],{"class":179,"line":284},[177,545,246],{"class":215},[177,547,220],{"class":219},[177,549,550,553,555],{"class":179,"line":292},[177,551,552],{"class":215},"      PGRST_DB_URI",[177,554,237],{"class":219},[177,556,557],{"class":187}," postgres:\u002F\u002Fauthenticator:authenticator@db:5432\u002Fpostgres\n",[177,559,560,563,565],{"class":179,"line":301},[177,561,562],{"class":215},"      PGRST_DB_SCHEMAS",[177,564,237],{"class":219},[177,566,567],{"class":187}," public\n",[177,569,570,573,575],{"class":179,"line":309},[177,571,572],{"class":215},"      PGRST_DB_ANON_ROLE",[177,574,237],{"class":219},[177,576,577],{"class":187}," anon\n",[177,579,580,583,585],{"class":179,"line":339},[177,581,582],{"class":215},"      PGRST_JWT_SECRET",[177,584,237],{"class":219},[177,586,587],{"class":187}," replace-with-a-secret-of-at-least-32-characters\n",[177,589,590],{"class":179,"line":350},[177,591,593],{"class":592},"sHwdD","      # Needed for the healthcheck below: --ready checks the admin server, which only\n",[177,595,596],{"class":179,"line":440},[177,597,598],{"class":592},"      # starts if given a port, and only accepts --ready's probe on a literal host\n",[177,600,601],{"class":179,"line":446},[177,602,603],{"class":592},"      # (not the default \"all interfaces\"). It isn't published, so this is internal-only.\n",[177,605,606,609,611],{"class":179,"line":452},[177,607,608],{"class":215},"      PGRST_ADMIN_SERVER_PORT",[177,610,237],{"class":219},[177,612,613],{"class":358}," 3002\n",[177,615,616,619,621],{"class":179,"line":458},[177,617,618],{"class":215},"      PGRST_ADMIN_SERVER_HOST",[177,620,237],{"class":219},[177,622,623],{"class":187}," localhost\n",[177,625,626,628,630,632,634,637,639],{"class":179,"line":464},[177,627,265],{"class":215},[177,629,237],{"class":219},[177,631,270],{"class":219},[177,633,273],{"class":219},[177,635,636],{"class":187},"3001:3000",[177,638,273],{"class":219},[177,640,281],{"class":219},[177,642,644,646],{"class":179,"line":643},18,[177,645,304],{"class":215},[177,647,220],{"class":219},[177,649,651,653,655,657,659,662,664,666,668,671,673,675,677,680,682],{"class":179,"line":650},19,[177,652,312],{"class":215},[177,654,237],{"class":219},[177,656,270],{"class":219},[177,658,273],{"class":219},[177,660,661],{"class":187},"CMD",[177,663,273],{"class":219},[177,665,326],{"class":219},[177,667,329],{"class":219},[177,669,670],{"class":187},"postgrest",[177,672,273],{"class":219},[177,674,326],{"class":219},[177,676,329],{"class":219},[177,678,679],{"class":187},"--ready",[177,681,273],{"class":219},[177,683,281],{"class":219},[177,685,687,689,691],{"class":179,"line":686},20,[177,688,342],{"class":215},[177,690,237],{"class":219},[177,692,347],{"class":187},[177,694,696,698,700],{"class":179,"line":695},21,[177,697,353],{"class":215},[177,699,237],{"class":219},[177,701,359],{"class":358},[196,703,705],{"id":704},"start-both-together","Start both together",[167,707,709],{"className":169,"code":708,"filename":171,"language":172,"meta":173,"style":173},"docker compose -f docker-compose.yml -f docker-compose.postgrest.yml up -d --wait\n",[90,710,711],{"__ignoreMap":173},[177,712,713,715,718,721,724,726,729,732,735],{"class":179,"line":180},[177,714,184],{"class":183},[177,716,717],{"class":187}," compose",[177,719,720],{"class":187}," -f",[177,722,723],{"class":187}," docker-compose.yml",[177,725,720],{"class":187},[177,727,728],{"class":187}," docker-compose.postgrest.yml",[177,730,731],{"class":187}," up",[177,733,734],{"class":187}," -d",[177,736,737],{"class":187}," --wait\n",[86,739,740,741,744,745,748,749,752,753,756],{},"Passing both files with ",[90,742,743],{},"-f"," merges them into one Compose project, so ",[90,746,747],{},"depends_on: db"," resolves and ",[90,750,751],{},"--wait"," blocks until ",[475,754,755],{},"both"," healthchecks pass, so there are no \"connection refused\" races. They're still separate files on disk; Compose is just running them together for convenience.",[196,758,760],{"id":759},"check-postgres-is-really-up","Check Postgres is really up",[167,762,764],{"className":169,"code":763,"filename":171,"language":172,"meta":173,"style":173},"docker compose exec db psql -U postgres -c 'select * from todos;'\n",[90,765,766],{"__ignoreMap":173},[177,767,768,770,772,775,778,781,784,787,790,792,795],{"class":179,"line":180},[177,769,184],{"class":183},[177,771,717],{"class":187},[177,773,774],{"class":187}," exec",[177,776,777],{"class":187}," db",[177,779,780],{"class":187}," psql",[177,782,783],{"class":187}," -U",[177,785,786],{"class":187}," postgres",[177,788,789],{"class":187}," -c",[177,791,329],{"class":219},[177,793,794],{"class":187},"select * from todos;",[177,796,797],{"class":219},"'\n",[86,799,800,801,804],{},"You should see the one seeded row (",[90,802,803],{},"Buy milk","). If this fails, PostgREST will fail too, so fix this first before going further.",[196,806,808],{"id":807},"check-postgrest-is-really-up","Check PostgREST is really up",[167,810,812],{"className":169,"code":811,"filename":171,"language":172,"meta":173,"style":173},"curl http:\u002F\u002Flocalhost:3001\u002F\n",[90,813,814],{"__ignoreMap":173},[177,815,816,818],{"class":179,"line":180},[177,817,92],{"class":183},[177,819,820],{"class":187}," http:\u002F\u002Flocalhost:3001\u002F\n",[86,822,823],{},"This returns PostgREST's auto-generated OpenAPI description as JSON, a wall of schema text. That's fine: it means PostgREST connected to Postgres and introspected it successfully. Now hit the actual table:",[167,825,827],{"className":169,"code":826,"filename":171,"language":172,"meta":173,"style":173},"curl http:\u002F\u002Flocalhost:3001\u002Ftodos\n",[90,828,829],{"__ignoreMap":173},[177,830,831,833],{"class":179,"line":180},[177,832,92],{"class":183},[177,834,835],{"class":187}," http:\u002F\u002Flocalhost:3001\u002Ftodos\n",[167,837,841],{"className":838,"code":839,"language":840,"meta":173,"style":173},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","[{\"id\":1,\"title\":\"Buy milk\"}]\n","json",[90,842,843],{"__ignoreMap":173},[177,844,845,848,851,855,857,859,862,864,866,869,871,873,875,877,879],{"class":179,"line":180},[177,846,847],{"class":219},"[{",[177,849,850],{"class":219},"\"",[177,852,854],{"class":853},"spNyl","id",[177,856,850],{"class":219},[177,858,237],{"class":219},[177,860,861],{"class":358},"1",[177,863,326],{"class":219},[177,865,850],{"class":219},[177,867,868],{"class":853},"title",[177,870,850],{"class":219},[177,872,237],{"class":219},[177,874,850],{"class":219},[177,876,803],{"class":187},[177,878,850],{"class":219},[177,880,881],{"class":219},"}]\n",[86,883,884,885,888,889,892,893,896,897,899,900,903],{},"That request had no ",[90,886,887],{},"Authorization"," header, so PostgREST ran it as ",[90,890,891],{},"anon",", which is exactly why the ",[90,894,895],{},"grant select ... to anon"," line in ",[90,898,364],{}," mattered. Remove that grant and this same request returns ",[90,901,902],{},"[]",".",[106,905,907],{"id":906},"one-postgres-many-databases","One Postgres, many databases",[86,909,910,911,914],{},"This is the actual point of keeping the two files separate: ",[97,912,913],{},"one Postgres instance can back any number of databases, and each database gets its own PostgREST container",", with its own port, its own JWT secret if you want one, and (once you put a reverse proxy in front of it) its own domain. You don't stand up a second Postgres per project or per customer; you add a database to the one you already have.",[86,916,917],{},"Add a second database on the same instance. Roles already exist cluster-wide, so this is all it takes:",[167,919,921],{"className":169,"code":920,"filename":171,"language":172,"meta":173,"style":173},"docker compose exec db psql -U postgres -c \"create database acme;\"\ndocker compose exec db psql -U postgres -d acme -c \"grant usage on schema public to anon, authenticated, service_role;\"\ndocker compose exec db psql -U postgres -d acme -c \"create table todos (id bigint generated always as identity primary key, title text not null); grant select, insert on todos to anon; insert into todos (title) values ('Acme first task');\"\n",[90,922,923,950,980],{"__ignoreMap":173},[177,924,925,927,929,931,933,935,937,939,941,944,947],{"class":179,"line":180},[177,926,184],{"class":183},[177,928,717],{"class":187},[177,930,774],{"class":187},[177,932,777],{"class":187},[177,934,780],{"class":187},[177,936,783],{"class":187},[177,938,786],{"class":187},[177,940,789],{"class":187},[177,942,943],{"class":219}," \"",[177,945,946],{"class":187},"create database acme;",[177,948,949],{"class":219},"\"\n",[177,951,952,954,956,958,960,962,964,966,968,971,973,975,978],{"class":179,"line":223},[177,953,184],{"class":183},[177,955,717],{"class":187},[177,957,774],{"class":187},[177,959,777],{"class":187},[177,961,780],{"class":187},[177,963,783],{"class":187},[177,965,786],{"class":187},[177,967,734],{"class":187},[177,969,970],{"class":187}," acme",[177,972,789],{"class":187},[177,974,943],{"class":219},[177,976,977],{"class":187},"grant usage on schema public to anon, authenticated, service_role;",[177,979,949],{"class":219},[177,981,982,984,986,988,990,992,994,996,998,1000,1002,1004,1007],{"class":179,"line":231},[177,983,184],{"class":183},[177,985,717],{"class":187},[177,987,774],{"class":187},[177,989,777],{"class":187},[177,991,780],{"class":187},[177,993,783],{"class":187},[177,995,786],{"class":187},[177,997,734],{"class":187},[177,999,970],{"class":187},[177,1001,789],{"class":187},[177,1003,943],{"class":219},[177,1005,1006],{"class":187},"create table todos (id bigint generated always as identity primary key, title text not null); grant select, insert on todos to anon; insert into todos (title) values ('Acme first task');",[177,1008,949],{"class":219},[86,1010,1011],{},"Then a second PostgREST file pointing at it, with the same shape as before, different database name, different port:",[167,1013,1016],{"className":205,"code":1014,"filename":1015,"language":208,"meta":173,"style":173},"services:\n  postgrest-acme:\n    image: postgrest\u002Fpostgrest:v16.3\n    depends_on:\n      db:\n        condition: service_healthy\n    environment:\n      PGRST_DB_URI: postgres:\u002F\u002Fauthenticator:authenticator@db:5432\u002Facme\n      PGRST_DB_SCHEMAS: public\n      PGRST_DB_ANON_ROLE: anon\n      PGRST_JWT_SECRET: replace-with-a-different-secret-of-32-plus-chars\n      PGRST_ADMIN_SERVER_PORT: 3004\n      PGRST_ADMIN_SERVER_HOST: localhost\n    ports: ['3003:3000']\n    healthcheck:\n      test: ['CMD', 'postgrest', '--ready']\n      interval: 2s\n      retries: 20\n","docker-compose.acme.yml",[90,1017,1018,1024,1031,1039,1045,1051,1059,1065,1074,1082,1090,1099,1108,1116,1133,1139,1171,1179],{"__ignoreMap":173},[177,1019,1020,1022],{"class":179,"line":180},[177,1021,216],{"class":215},[177,1023,220],{"class":219},[177,1025,1026,1029],{"class":179,"line":223},[177,1027,1028],{"class":215},"  postgrest-acme",[177,1030,220],{"class":219},[177,1032,1033,1035,1037],{"class":179,"line":231},[177,1034,234],{"class":215},[177,1036,237],{"class":219},[177,1038,517],{"class":187},[177,1040,1041,1043],{"class":179,"line":243},[177,1042,522],{"class":215},[177,1044,220],{"class":219},[177,1046,1047,1049],{"class":179,"line":251},[177,1048,529],{"class":215},[177,1050,220],{"class":219},[177,1052,1053,1055,1057],{"class":179,"line":262},[177,1054,536],{"class":215},[177,1056,237],{"class":219},[177,1058,541],{"class":187},[177,1060,1061,1063],{"class":179,"line":284},[177,1062,246],{"class":215},[177,1064,220],{"class":219},[177,1066,1067,1069,1071],{"class":179,"line":292},[177,1068,552],{"class":215},[177,1070,237],{"class":219},[177,1072,1073],{"class":187}," postgres:\u002F\u002Fauthenticator:authenticator@db:5432\u002Facme\n",[177,1075,1076,1078,1080],{"class":179,"line":301},[177,1077,562],{"class":215},[177,1079,237],{"class":219},[177,1081,567],{"class":187},[177,1083,1084,1086,1088],{"class":179,"line":309},[177,1085,572],{"class":215},[177,1087,237],{"class":219},[177,1089,577],{"class":187},[177,1091,1092,1094,1096],{"class":179,"line":339},[177,1093,582],{"class":215},[177,1095,237],{"class":219},[177,1097,1098],{"class":187}," replace-with-a-different-secret-of-32-plus-chars\n",[177,1100,1101,1103,1105],{"class":179,"line":350},[177,1102,608],{"class":215},[177,1104,237],{"class":219},[177,1106,1107],{"class":358}," 3004\n",[177,1109,1110,1112,1114],{"class":179,"line":440},[177,1111,618],{"class":215},[177,1113,237],{"class":219},[177,1115,623],{"class":187},[177,1117,1118,1120,1122,1124,1126,1129,1131],{"class":179,"line":446},[177,1119,265],{"class":215},[177,1121,237],{"class":219},[177,1123,270],{"class":219},[177,1125,273],{"class":219},[177,1127,1128],{"class":187},"3003:3000",[177,1130,273],{"class":219},[177,1132,281],{"class":219},[177,1134,1135,1137],{"class":179,"line":452},[177,1136,304],{"class":215},[177,1138,220],{"class":219},[177,1140,1141,1143,1145,1147,1149,1151,1153,1155,1157,1159,1161,1163,1165,1167,1169],{"class":179,"line":458},[177,1142,312],{"class":215},[177,1144,237],{"class":219},[177,1146,270],{"class":219},[177,1148,273],{"class":219},[177,1150,661],{"class":187},[177,1152,273],{"class":219},[177,1154,326],{"class":219},[177,1156,329],{"class":219},[177,1158,670],{"class":187},[177,1160,273],{"class":219},[177,1162,326],{"class":219},[177,1164,329],{"class":219},[177,1166,679],{"class":187},[177,1168,273],{"class":219},[177,1170,281],{"class":219},[177,1172,1173,1175,1177],{"class":179,"line":464},[177,1174,342],{"class":215},[177,1176,237],{"class":219},[177,1178,347],{"class":187},[177,1180,1181,1183,1185],{"class":179,"line":643},[177,1182,353],{"class":215},[177,1184,237],{"class":219},[177,1186,359],{"class":358},[86,1188,1189],{},"Bring it up alongside the other two files:",[167,1191,1193],{"className":169,"code":1192,"filename":171,"language":172,"meta":173,"style":173},"docker compose -f docker-compose.yml -f docker-compose.postgrest.yml -f docker-compose.acme.yml up -d --wait\n",[90,1194,1195],{"__ignoreMap":173},[177,1196,1197,1199,1201,1203,1205,1207,1209,1211,1214,1216,1218],{"class":179,"line":180},[177,1198,184],{"class":183},[177,1200,717],{"class":187},[177,1202,720],{"class":187},[177,1204,723],{"class":187},[177,1206,720],{"class":187},[177,1208,728],{"class":187},[177,1210,720],{"class":187},[177,1212,1213],{"class":187}," docker-compose.acme.yml",[177,1215,731],{"class":187},[177,1217,734],{"class":187},[177,1219,737],{"class":187},[86,1221,1222],{},"Both APIs are now live, each scoped to its own database:",[167,1224,1226],{"className":169,"code":1225,"filename":171,"language":172,"meta":173,"style":173},"curl http:\u002F\u002Flocalhost:3001\u002Ftodos   # [{\"id\":1,\"title\":\"Buy milk\"}]\ncurl http:\u002F\u002Flocalhost:3003\u002Ftodos   # [{\"id\":1,\"title\":\"Acme first task\"}]\n",[90,1227,1228,1238],{"__ignoreMap":173},[177,1229,1230,1232,1235],{"class":179,"line":180},[177,1231,92],{"class":183},[177,1233,1234],{"class":187}," http:\u002F\u002Flocalhost:3001\u002Ftodos",[177,1236,1237],{"class":592},"   # [{\"id\":1,\"title\":\"Buy milk\"}]\n",[177,1239,1240,1242,1245],{"class":179,"line":223},[177,1241,92],{"class":183},[177,1243,1244],{"class":187}," http:\u002F\u002Flocalhost:3003\u002Ftodos",[177,1246,1247],{"class":592},"   # [{\"id\":1,\"title\":\"Acme first task\"}]\n",[86,1249,1250,1251,1254],{},"Repeat that pattern (one ",[90,1252,1253],{},"create database",", one Compose file, one port) for every database you add. In production, the last step is a reverse proxy (Caddy, nginx, Traefik) mapping a domain to each PostgREST container's port; that's ordinary HTTP routing and outside PostgREST's own concerns, so it isn't covered here.",[106,1256,1258],{"id":1257},"where-jwts-fit-in","Where JWTs fit in",[86,1260,1261,1262,1265,1266,1268,1269,1272,1273,1276],{},"PostgREST never sees \"users\". It only sees a JWT's ",[90,1263,1264],{},"role"," claim, which picks the Postgres role for that one request, and everything else in the payload is fair game for row-level-security policies to read. There's no session, no cookie store, no login endpoint inside PostgREST itself; minting and verifying JWTs is entirely your app's job (see ",[125,1267,36],{"href":37}," for how this module helps with ",[90,1270,1271],{},"mintJwt()"," and ",[90,1274,1275],{},"setPostgrestToken()",").",[86,1278,1279,1280,1283],{},"You can mint a throwaway token to try ",[90,1281,1282],{},"authenticated"," locally with plain Node, no dependencies:",[167,1285,1287],{"className":169,"code":1286,"filename":171,"language":172,"meta":173,"style":173},"node -e \"\nconst c=require('crypto'),b=s=>Buffer.from(s).toString('base64url'),\nh=b(JSON.stringify({alg:'HS256',typ:'JWT'})),\np=b(JSON.stringify({role:'authenticated'})),\nsig=c.createHmac('sha256','replace-with-a-secret-of-at-least-32-characters').update(h+'.'+p).digest('base64url');\nconsole.log(h+'.'+p+'.'+sig)\n\"\n",[90,1288,1289,1300,1305,1310,1315,1320,1325],{"__ignoreMap":173},[177,1290,1291,1294,1297],{"class":179,"line":180},[177,1292,1293],{"class":183},"node",[177,1295,1296],{"class":187}," -e",[177,1298,1299],{"class":219}," \"\n",[177,1301,1302],{"class":179,"line":223},[177,1303,1304],{"class":187},"const c=require('crypto'),b=s=>Buffer.from(s).toString('base64url'),\n",[177,1306,1307],{"class":179,"line":231},[177,1308,1309],{"class":187},"h=b(JSON.stringify({alg:'HS256',typ:'JWT'})),\n",[177,1311,1312],{"class":179,"line":243},[177,1313,1314],{"class":187},"p=b(JSON.stringify({role:'authenticated'})),\n",[177,1316,1317],{"class":179,"line":251},[177,1318,1319],{"class":187},"sig=c.createHmac('sha256','replace-with-a-secret-of-at-least-32-characters').update(h+'.'+p).digest('base64url');\n",[177,1321,1322],{"class":179,"line":262},[177,1323,1324],{"class":187},"console.log(h+'.'+p+'.'+sig)\n",[177,1326,1327],{"class":179,"line":284},[177,1328,949],{"class":219},[167,1330,1332],{"className":169,"code":1331,"filename":171,"language":172,"meta":173,"style":173},"curl http:\u002F\u002Flocalhost:3001\u002Ftodos -H \"Authorization: Bearer \u003Cpaste the token>\"\n",[90,1333,1334],{"__ignoreMap":173},[177,1335,1336,1338,1340,1343,1345,1348],{"class":179,"line":180},[177,1337,92],{"class":183},[177,1339,1234],{"class":187},[177,1341,1342],{"class":187}," -H",[177,1344,943],{"class":219},[177,1346,1347],{"class":187},"Authorization: Bearer \u003Cpaste the token>",[177,1349,949],{"class":219},[106,1351,1353],{"id":1352},"troubleshooting","Troubleshooting",[1355,1356,1357],"note",{},"Every one of these is a real error message you'll hit, not a hypothetical.",[111,1359,1360,1389,1401,1418,1432],{},[114,1361,1362,1367,1368,1370,1371,1374,1375,1377,1378,1381,1382,1384,1385,1388],{},[97,1363,1364],{},[90,1365,1366],{},"password authentication failed for user \"authenticator\"",": ",[90,1369,364],{}," only runs the ",[475,1372,1373],{},"first"," time the ",[90,1376,368],{}," volume is created. If you edited it after the first ",[90,1379,1380],{},"docker compose up",", run ",[90,1383,103],{}," (the ",[90,1386,1387],{},"-v"," drops the volume) and start again.",[114,1390,1391,1396,1397,1400],{},[97,1392,1393],{},[90,1394,1395],{},"JWT secret must be at least 32 characters",": PostgREST refuses to boot with a short ",[90,1398,1399],{},"PGRST_JWT_SECRET",". Not a real security boundary locally, but PostgREST enforces it anyway; pad the string out.",[114,1402,1403,1409,1410,1413,1414,1417],{},[97,1404,1405,1408],{},[90,1406,1407],{},"bind: address already in use"," on port 5432",": you already have a Postgres running natively (common on macOS via Homebrew) or a previous Compose project still up. Either stop that service, or remap the port (e.g. ",[90,1411,1412],{},"'5433:5432'",") and update ",[90,1415,1416],{},"PGRST_DB_URI","'s port to match.",[114,1419,1420,1425,1426,1428,1429,903],{},[97,1421,1422],{},[90,1423,1424],{},"relation \"todos\" does not exist",": same root cause as the first bullet: ",[90,1427,364],{}," didn't run against a fresh volume. ",[90,1430,1431],{},"docker compose down -v && docker compose up -d --wait",[114,1433,1434,1439,1440,1443,1444,1446,1447,1450,1451,1453,1454,1456],{},[97,1435,1436],{},[90,1437,1438],{},"curl: (7) Failed to connect",": check ",[90,1441,1442],{},"docker compose ps","; if ",[90,1445,670],{}," shows as unhealthy, run ",[90,1448,1449],{},"docker compose logs postgrest","; it almost always means it couldn't reach ",[90,1452,368],{}," yet, ",[90,1455,1416],{}," is wrong, or (for a database you just added) the database doesn't exist yet.",[106,1458,1460],{"id":1459},"resetting","Resetting",[167,1462,1464],{"className":169,"code":1463,"filename":171,"language":172,"meta":173,"style":173},"docker compose -f docker-compose.yml -f docker-compose.postgrest.yml down -v   # stop and wipe everything, including the Postgres volume\ndocker compose -f docker-compose.yml -f docker-compose.postgrest.yml up -d --wait\n",[90,1465,1466,1489],{"__ignoreMap":173},[177,1467,1468,1470,1472,1474,1476,1478,1480,1483,1486],{"class":179,"line":180},[177,1469,184],{"class":183},[177,1471,717],{"class":187},[177,1473,720],{"class":187},[177,1475,723],{"class":187},[177,1477,720],{"class":187},[177,1479,728],{"class":187},[177,1481,1482],{"class":187}," down",[177,1484,1485],{"class":187}," -v",[177,1487,1488],{"class":592},"   # stop and wipe everything, including the Postgres volume\n",[177,1490,1491,1493,1495,1497,1499,1501,1503,1505,1507],{"class":179,"line":223},[177,1492,184],{"class":183},[177,1494,717],{"class":187},[177,1496,720],{"class":187},[177,1498,723],{"class":187},[177,1500,720],{"class":187},[177,1502,728],{"class":187},[177,1504,731],{"class":187},[177,1506,734],{"class":187},[177,1508,737],{"class":187},[1510,1511,1512,1513,1515,1516,1518,1519,1522,1523,1526,1527,1530],"tip",{},"This module's own repository ships a fuller version of this exact setup (seeded tables, row-level-security policies, a second schema), split the same way into ",[90,1514,207],{}," (Postgres) and ",[90,1517,493],{}," (PostgREST), seeded by ",[90,1520,1521],{},"db\u002Fseed.sql",". It's a good base for bug reproductions, and it's what ",[90,1524,1525],{},"pnpm db:up"," \u002F ",[90,1528,1529],{},"pnpm test"," run against.",[86,1532,1533,1534,1537,1538,1540,1541,1544,1545,903],{},"Once ",[90,1535,1536],{},"curl http:\u002F\u002Flocalhost:3001\u002Ftodos"," works, move on to ",[125,1539,16],{"href":17}," and set ",[90,1542,1543],{},"NUXT_PUBLIC_POSTGREST_URL"," to ",[90,1546,1547],{},"http:\u002F\u002Flocalhost:3001",[1549,1550,1551],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}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 .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"title":173,"searchDepth":223,"depth":223,"links":1553},[1554,1555,1562,1563,1564,1565],{"id":108,"depth":223,"text":109},{"id":149,"depth":223,"text":150,"children":1556},[1557,1558,1559,1560,1561],{"id":198,"depth":231,"text":199},{"id":485,"depth":231,"text":486},{"id":704,"depth":231,"text":705},{"id":759,"depth":231,"text":760},{"id":807,"depth":231,"text":808},{"id":906,"depth":223,"text":907},{"id":1257,"depth":223,"text":1258},{"id":1352,"depth":223,"text":1353},{"id":1459,"depth":223,"text":1460},"A from-scratch, no-assumptions guide to running Postgres and PostgREST locally with Docker.","md",null,{},{"icon":54},{"title":51,"description":1566},"lLFZfIkaU0BakeBRbEVJV5Hn8VeRDo0w3dAnC1vuSec",[1574,1576],{"title":41,"path":42,"stem":43,"description":1575,"icon":44,"children":-1},"Type every table, column and schema from your database.",{"title":56,"path":57,"stem":58,"description":1577,"icon":59,"children":-1},"Map @nuxtjs\u002Fsupabase patterns to Nuxt PostgREST.",1792365098363]