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