{
  "slug": "pocketbase",
  "name": "PocketBase",
  "summary": "One small open-source file that is the database, sign-in and admin API. Self-host it on a cheap server and keep everything in one place.",
  "audience": [
    "developers"
  ],
  "bestFor": "Low running costs and full control: one business, one small server, simple backups.",
  "cost": "Free and open source; a small VPS from a few pounds a month.",
  "rank": 6,
  "covers": "full",
  "docs": [
    {
      "label": "PocketBase API rules",
      "href": "https://pocketbase.io/docs/api-rules-and-filters/"
    },
    {
      "label": "Authentication",
      "href": "https://pocketbase.io/docs/authentication/"
    }
  ],
  "body": "## 1. Collections\n\nCreate one collection per data-model table in the PocketBase dashboard. Instead of a separate `staff_members` table, add to the built-in **users** auth collection a `role` select field (`owner`, `manager`, `staff`) and a `status` field.\n\n## 2. API rules\n\nPocketBase checks these on every request. Set them per collection (List/View, Create, Update, Delete); an empty rule means \"anyone\", a locked rule means \"only superusers\".\n\n| Collection | List/View | Create | Update | Delete |\n| --- | --- | --- | --- | --- |\n| menu_sections, menu_items, opening_hours, closures | *(empty — public)* | `@request.auth.role ?= \"owner\" \\|\\| @request.auth.role ?= \"manager\"` | same | same |\n| settings | *(empty — public)* | locked | `@request.auth.role = \"owner\"` | locked |\n| bookings | `@request.auth.id != \"\"` | `@request.body.status = \"confirmed\"` | `@request.auth.role = \"owner\" \\|\\| @request.auth.role = \"manager\" \\|\\| (@request.auth.role = \"staff\" && @request.body.name:isset = false && @request.body.starts_at:isset = false)` | `@request.auth.role = \"owner\" \\|\\| @request.auth.role = \"manager\"` |\n| orders | `@request.auth.id != \"\"` | `@request.body.stage = \"new\" && @request.body.paid = false` | same pattern as bookings (staff may change `stage` only) | owner/manager |\n| enquiries | owner/manager | `@request.body.status = \"new\"` | owner/manager | owner/manager |\n| customers, reviews | owner/manager | owner/manager | owner/manager | owner/manager |\n| users | `id = @request.auth.id \\|\\| @request.auth.role = \"owner\" \\|\\| @request.auth.role = \"manager\"` | locked (invite from server) | `@request.auth.role = \"owner\"` | `@request.auth.role = \"owner\"` |\n\n\"owner/manager\" means `@request.auth.role = \"owner\" || @request.auth.role = \"manager\"`. Also stop people from changing their own role: on the users Update rule add `&& (@request.body.role:isset = false || @request.auth.role = \"owner\")`.\n\n## 3. The first owner\n\nCreate the owner as a normal user in the dashboard and set `role` to `owner` there.\n\n## 4. Protect /admin\n\nSign in with `pb.collection(\"users\").authWithPassword(...)` (or OTP). For server-rendered admin pages, keep the auth token in an HTTP-only cookie (`pb.authStore.exportToCookie()`), load it in middleware, call `pb.collection(\"users\").authRefresh()`, and redirect to `/admin/sign-in` when it fails. The API rules are what protect the data.\n\n## 5. Run it safely\n\nPut PocketBase behind HTTPS (Caddy or the host's proxy), keep the superuser dashboard password strong, and back up `pb_data` daily.",
  "url": "https://copypasteui.dev/r/recipes/pocketbase.json",
  "prompt": "# Back office with PocketBase\n\nSet up the back office for this site with this backend, following every step. The back-office screens are the CopyPasteUI \"admin\" components (admin-sign-in, admin-overview, admin-bookings …); each takes its data and save actions as props. Best for: Low running costs and full control: one business, one small server, simple backups. Cost: Free and open source; a small VPS from a few pounds a month.\n\n## The back-office data model\n\nEvery recipe stores the same data with the same names, so the back-office screens (category \"admin\") work with any backend. Money is stored in pence/cents as whole numbers. Times are stored in UTC.\n\n| Table | Fields | Used by |\n| --- | --- | --- |\n| `staff_members` | user_id (the auth user), email, name, role (`owner` \\| `manager` \\| `staff`), status (`active` \\| `invited`) | admin-staff, every access check |\n| `settings` | one row: name, phone, email, address, instagram, booking_link, notify_bookings, notify_enquiries, notify_reviews, daily_summary | admin-settings, the public header/footer/contact |\n| `bookings` | id, starts_at, name, party, phone, email, table_label, note, status (`confirmed` \\| `arrived` \\| `no-show` \\| `cancelled`), created_at | admin-bookings, admin-overview |\n| `orders` | id, number, customer_name, placed_at, pickup_at, items (list of {name, qty}), total_pence, paid, stage (`new` \\| `preparing` \\| `ready` \\| `collected`) | admin-orders |\n| `enquiries` | id, name, email, phone, subject, message, received_at, status (`new` \\| `replied` \\| `archived`) | admin-enquiries |\n| `menu_sections` | id, name, position | admin-menu-editor, the public menu |\n| `menu_items` | id, section_id, name, description, price_pence, tags (list), available, position | admin-menu-editor, the public menu |\n| `customers` | id, name, email, phone, visits, last_visit, spend_pence, notes, marketing_opt_in | admin-customers |\n| `reviews` | id, source, author, rating (1–5), text, posted_at, reply | admin-reviews |\n| `opening_hours` | day (0 = Monday … 6 = Sunday), open, opens, closes | admin-hours, the public footer/contact |\n| `closures` | id, starts_on, ends_on, note | admin-hours, the closed-notice banner |\n\n## Who may do what\n\nThis is the security contract. Implement it in the database's own rules, not only in the interface — hiding a button doesn't stop anyone.\n\n| Data | Public visitors | Staff | Manager | Owner |\n| --- | --- | --- | --- | --- |\n| menu_sections, menu_items, opening_hours, closures | read | read | read, write | read, write |\n| settings | read (public fields only) | read | read | read, write |\n| bookings | create only (from the booking form), never read | read, change status | read, write | read, write |\n| orders | create only (from checkout), never read | read, change stage | read, write | read, write |\n| enquiries | create only (from the contact form), never read | — | read, write | read, write |\n| customers, reviews | — | — | read, write | read, write |\n| staff_members | — | read own row | read | read, write |\n\nRules that apply to every backend:\n- Every `/admin` page checks the session **on the server** and sends signed-out people to `/admin/sign-in`.\n- Secret keys (service-role keys, admin SDK credentials, API secrets) live only in server code and environment variables, never in the browser bundle.\n- Public forms may only insert, with fields limited to what the form sends; they can never read other people's rows.\n- `/admin` is left out of the navigation and the sitemap, and disallowed in robots.txt.\n- The first owner account is created by hand once (instructions in each recipe); everyone else is invited from admin-staff.\n\n# PocketBase: steps\n\n## 1. Collections\n\nCreate one collection per data-model table in the PocketBase dashboard. Instead of a separate `staff_members` table, add to the built-in **users** auth collection a `role` select field (`owner`, `manager`, `staff`) and a `status` field.\n\n## 2. API rules\n\nPocketBase checks these on every request. Set them per collection (List/View, Create, Update, Delete); an empty rule means \"anyone\", a locked rule means \"only superusers\".\n\n| Collection | List/View | Create | Update | Delete |\n| --- | --- | --- | --- | --- |\n| menu_sections, menu_items, opening_hours, closures | *(empty — public)* | `@request.auth.role ?= \"owner\" \\|\\| @request.auth.role ?= \"manager\"` | same | same |\n| settings | *(empty — public)* | locked | `@request.auth.role = \"owner\"` | locked |\n| bookings | `@request.auth.id != \"\"` | `@request.body.status = \"confirmed\"` | `@request.auth.role = \"owner\" \\|\\| @request.auth.role = \"manager\" \\|\\| (@request.auth.role = \"staff\" && @request.body.name:isset = false && @request.body.starts_at:isset = false)` | `@request.auth.role = \"owner\" \\|\\| @request.auth.role = \"manager\"` |\n| orders | `@request.auth.id != \"\"` | `@request.body.stage = \"new\" && @request.body.paid = false` | same pattern as bookings (staff may change `stage` only) | owner/manager |\n| enquiries | owner/manager | `@request.body.status = \"new\"` | owner/manager | owner/manager |\n| customers, reviews | owner/manager | owner/manager | owner/manager | owner/manager |\n| users | `id = @request.auth.id \\|\\| @request.auth.role = \"owner\" \\|\\| @request.auth.role = \"manager\"` | locked (invite from server) | `@request.auth.role = \"owner\"` | `@request.auth.role = \"owner\"` |\n\n\"owner/manager\" means `@request.auth.role = \"owner\" || @request.auth.role = \"manager\"`. Also stop people from changing their own role: on the users Update rule add `&& (@request.body.role:isset = false || @request.auth.role = \"owner\")`.\n\n## 3. The first owner\n\nCreate the owner as a normal user in the dashboard and set `role` to `owner` there.\n\n## 4. Protect /admin\n\nSign in with `pb.collection(\"users\").authWithPassword(...)` (or OTP). For server-rendered admin pages, keep the auth token in an HTTP-only cookie (`pb.authStore.exportToCookie()`), load it in middleware, call `pb.collection(\"users\").authRefresh()`, and redirect to `/admin/sign-in` when it fails. The API rules are what protect the data.\n\n## 5. Run it safely\n\nPut PocketBase behind HTTPS (Caddy or the host's proxy), keep the superuser dashboard password strong, and back up `pb_data` daily.\n\nDocs to check before you start, since providers change their APIs: PocketBase API rules (https://pocketbase.io/docs/api-rules-and-filters/); Authentication (https://pocketbase.io/docs/authentication/).\n"
}
