[{"data":1,"prerenderedAt":1202},["ShallowReactive",2],{"navigation":3,"\u002Fplugins\u002Fapi-keys":189,"\u002Fplugins\u002Fapi-keys-surround":1197},[4,23,40,51,74,116,157,177],{"title":5,"path":6,"stem":7,"children":8,"icon":22},"Get Started","\u002Fget-started","1.get-started\u002F1.index",[9,12,17],{"title":10,"path":6,"stem":7,"icon":11},"Introduction","i-lucide-house",{"title":13,"path":14,"stem":15,"icon":16},"Prerequisites","\u002Fget-started\u002Fprerequisites","1.get-started\u002F2.prerequisites","i-lucide-list-checks",{"title":18,"path":19,"stem":20,"icon":21},"Installation","\u002Fget-started\u002Finstallation","1.get-started\u002F3.installation","i-lucide-settings","i-lucide-rocket",{"title":24,"icon":25,"path":26,"stem":27,"children":28,"page":39},"Develop","i-lucide-code","\u002Fdevelop","2.develop",[29,34],{"title":30,"path":31,"stem":32,"icon":33},"Version Control","\u002Fdevelop\u002Fversion-control","2.develop\u002F1.version-control","i-lucide-git-branch",{"title":35,"path":36,"stem":37,"icon":38},"Claude Code","\u002Fdevelop\u002Fclaude-code","2.develop\u002F2.claude-code","i-lucide-sparkles",false,{"title":41,"icon":42,"path":43,"stem":44,"children":45,"page":39},"Launch","i-lucide-globe","\u002Flaunch","3.launch",[46],{"title":47,"path":48,"stem":49,"icon":50},"Cloudflare","\u002Flaunch\u002Fcloudflare","3.launch\u002F1.cloudflare","i-lucide-cloud-upload",{"title":52,"path":53,"stem":54,"children":55,"icon":73},"Plugins","\u002Fplugins","4.plugins\u002F1.index",[56,58,63,68],{"title":52,"path":53,"stem":54,"icon":57},"i-lucide-list",{"title":59,"path":60,"stem":61,"icon":62},"Public API","\u002Fplugins\u002Fapi-keys","4.plugins\u002F2.api-keys","i-lucide-key",{"title":64,"path":65,"stem":66,"icon":67},"Cron Jobs","\u002Fplugins\u002Fcron-jobs","4.plugins\u002F4.cron-jobs","i-lucide-clock",{"title":69,"path":70,"stem":71,"icon":72},"Rate Limiting","\u002Fplugins\u002Frate-limiting","4.plugins\u002F5.rate-limiting","i-lucide-gauge","i-lucide-puzzle",{"title":75,"path":76,"stem":77,"children":78,"icon":115},"Examples","\u002Fexamples","5.examples\u002F1.index",[79,80,85,90,95,100,105,110],{"title":75,"path":76,"stem":77,"icon":57},{"title":81,"path":82,"stem":83,"icon":84},"Job Management","\u002Fexamples\u002Fjob-management","5.examples\u002F2.job-management","i-lucide-briefcase",{"title":86,"path":87,"stem":88,"icon":89},"Kanban \u002F To-Do List","\u002Fexamples\u002Fkanban-todo","5.examples\u002F3.kanban-todo","i-lucide-kanban",{"title":91,"path":92,"stem":93,"icon":94},"Inventory Management","\u002Fexamples\u002Finventory-management","5.examples\u002F4.inventory-management","i-lucide-package",{"title":96,"path":97,"stem":98,"icon":99},"Mini CRM","\u002Fexamples\u002Fmini-crm","5.examples\u002F5.mini-crm","i-lucide-users",{"title":101,"path":102,"stem":103,"icon":104},"Sales Orders & Invoices","\u002Fexamples\u002Fsales-invoices","5.examples\u002F6.sales-invoices","i-lucide-receipt",{"title":106,"path":107,"stem":108,"icon":109},"Calendar & Booking","\u002Fexamples\u002Fcalendar-booking","5.examples\u002F7.calendar-booking","i-lucide-calendar",{"title":111,"path":112,"stem":113,"icon":114},"Support Tickets","\u002Fexamples\u002Fsupport-tickets","5.examples\u002F8.support-tickets","i-lucide-life-buoy","i-lucide-book-open",{"title":117,"icon":118,"path":119,"stem":120,"children":121,"page":39},"Reference","i-lucide-file-text","\u002Freference","6.reference",[122,127,132,137,142,147,152],{"title":123,"path":124,"stem":125,"icon":126},"Architecture","\u002Freference\u002Farchitecture","6.reference\u002F1.architecture","i-lucide-layers",{"title":128,"path":129,"stem":130,"icon":131},"Permissions","\u002Freference\u002Fpermissions","6.reference\u002F2.permissions","i-lucide-shield",{"title":133,"path":134,"stem":135,"icon":136},"Invitations","\u002Freference\u002Finvitations","6.reference\u002F3.invitations","i-lucide-mail",{"title":138,"path":139,"stem":140,"icon":141},"Webhooks","\u002Freference\u002Fwebhooks","6.reference\u002F4.webhooks","i-lucide-webhook",{"title":143,"path":144,"stem":145,"icon":146},"AI Chat","\u002Freference\u002Fai-chat","6.reference\u002F5.ai-chat","i-lucide-message-square",{"title":148,"path":149,"stem":150,"icon":151},"Activity Log","\u002Freference\u002Factivity-log","6.reference\u002F6.activity-log","i-lucide-scroll",{"title":153,"path":154,"stem":155,"icon":156},"Manual Setup","\u002Freference\u002Fmanual-setup","6.reference\u002F7.manual-setup","i-lucide-wrench",{"title":158,"icon":159,"path":160,"stem":161,"children":162,"page":39},"Legal","i-lucide-scale","\u002Flegal","7.legal",[163,168,172],{"title":164,"path":165,"stem":166,"icon":167},"License","\u002Flegal\u002Flicense","7.legal\u002F1.license","i-lucide-file-check",{"title":169,"path":170,"stem":171,"icon":118},"Terms and Conditions","\u002Flegal\u002Fterms","7.legal\u002F2.terms",{"title":173,"path":174,"stem":175,"icon":176},"Privacy Policy","\u002Flegal\u002Fprivacy","7.legal\u002F3.privacy","i-lucide-lock",{"title":178,"path":179,"stem":180,"children":181,"icon":183},"Upgrades","\u002Fupgrades","8.upgrades\u002F1.index",[182,184],{"title":178,"path":179,"stem":180,"icon":183},"i-lucide-arrow-up-circle",{"title":185,"path":186,"stem":187,"icon":188},"\u002Fapp\u002F* gated subtree routing","\u002Fupgrades\u002Fapp-subtree-routing","8.upgrades\u002F2.app-subtree-routing","i-lucide-route",{"id":190,"title":59,"body":191,"description":1190,"extension":1191,"links":1192,"meta":1193,"navigation":1194,"path":60,"seo":1195,"stem":61,"__hash__":1196},"docs\u002F4.plugins\u002F2.api-keys.md",{"type":192,"value":193,"toc":1188},"minimark",[194,209,212,1184],[195,196,197],"note",{},[198,199,200,204,205,208],"p",{},[201,202,203],"strong",{},"Prerequisite:"," Complete the ",[206,207,5],"a",{"href":6}," guide first.",[198,210,211],{},"To install this plugin, copy the first prompt, paste it into Claude Code (or Codex, or any AI coding tool), let it finish, then run the next prompt.",[213,214,216,221,792,796,1071],"steps",{"level":215},"3",[217,218,220],"h3",{"id":219},"api-keys","API Keys",[222,223,228],"pre",{"className":224,"code":225,"language":226,"meta":227,"style":227},"language-txt shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","Build the API key management system so external services can authenticate\nwith the app.\n\nPermissions:\n\nAdd these keys to `shared\u002Fpermissions.ts` if they don't already exist:\n\n```\n\"api_keys.view\": [\"owner\", \"admin\"],\n\"api_keys.create\": [\"owner\", \"admin\"],\n\"api_keys.revoke\": [\"owner\", \"admin\"],\n```\n\nDatabase (via Supabase MCP):\n\nCreate an `api_keys` table — id (uuid, default gen_random_uuid(), primary\nkey), team_id (uuid, references teams(id) on delete cascade, not null),\ncreated_by (uuid, references profiles(id), not null), name (text, not null),\nkey_prefix (text, not null), key_hash (text, not null), last_used_at\n(timestamptz, nullable), expires_at (timestamptz, nullable), revoked_at\n(timestamptz, nullable), created_at (timestamptz, default now()).\n\nEnable RLS. Add a SELECT-only policy using `is_team_member()` scoped to\n`team_id` — follow the `announcements_team_member_read` pattern in\n`supabase\u002Fmigrations\u002F00001_initial_schema.sql`. Writes go through the\nservice-role server routes below, so no insert\u002Fupdate\u002Fdelete RLS policies\nare needed.\n\nEnable the activity log for the table, excluding the secret hash:\n\n```sql\nselect enable_activity_log('api_keys', exclude_cols => array['key_hash']);\n```\n\nServer routes (all use `authUser`):\n\n- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fapi-keys` — uses\n  `authUser(event, \"api_keys.view\")`. Returns all API keys for the team\n  (NEVER return key_hash in the response), ordered by created_at desc.\n  Include the creator's name by joining profiles.\n\n- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fapi-keys` — uses\n  `authUser(event, \"api_keys.create\")`. Reads { name } from the body.\n  Generates a new API key:\n  1. Generate 32 random bytes using Node's `crypto.randomBytes`\n  2. Base64url encode the bytes to create the raw key string\n  3. Create a prefix from the first 8 characters of the encoded key\n  4. Hash the full key with SHA-256 using Node's `crypto.createHash`\n  5. Convert the hash to a hex string\n  6. Store team_id, name, key_prefix, key_hash, and created_by\n  7. Return the full key in the response — this is the ONLY time it is\n     ever visible. The server never stores the raw key.\n\n- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Fapi-keys\u002F[keyId]` — uses\n  `authUser(event, \"api_keys.revoke\")`. Sets revoked_at to now. Validates\n  the key belongs to the team and is not already revoked.\n\nUI:\n\nAdd an \"API Keys\" tab to the settings page. Create\n`app\u002Fpages\u002Fapp\u002Fsettings\u002Fapi-keys.vue` (settings uses a `settings.vue` parent\nwith file-based child routes) and register the tab in the `links` computed\nin `app\u002Fpages\u002Fapp\u002Fsettings.vue`. Gate the tab with `can(\"api_keys.view\")` so\nonly owners\u002Fadmins see it. Use `definePageMeta({ middleware:\n[requirePermission(\"api_keys.view\", \"\u002Fapp\u002Fsettings\")] })` on the page itself.\n\nThe API Keys settings tab should:\n\n- List all keys in a table showing: name, prefix (displayed as\n  `sk_...{prefix}`), created by (name), created date, last used date\n  (or \"Never\"), and status. Status is a badge: \"Active\" (green) if not\n  revoked and not expired, \"Revoked\" (red) if revoked_at is set,\n  \"Expired\" (neutral) if expires_at is in the past.\n\n- A \"Create Key\" button at the top right, wrapped in\n  `CanAccess permission=\"api_keys.create\"`. Opens a `UModal` with a\n  single field: key name (required). Show loading on submit.\n\n- After creation, show a second modal displaying the full API key with\n  a copy-to-clipboard button and a warning message: \"Copy this key now.\n  You will not be able to see it again.\" The modal should only be\n  dismissible after the user has seen the key.\n\n- Each active key row has a \"Revoke\" button wrapped in\n  `CanAccess permission=\"api_keys.revoke\"`. Clicking it shows a\n  confirmation modal before revoking. Show per-row loading state.\n\n- Show an empty state when no keys exist with a message like\n  \"No API keys yet. Create one to allow external services to access\n  your data.\"\n\n- Show `USkeleton` placeholders on initial load.\n\nUse the icon `i-solar-key-bold-duotone` for the settings tab entry.\n\nRegenerate TypeScript types via Supabase MCP and save to\n`shared\u002Ftypes\u002Fdatabase.types.ts`.\n","txt","",[229,230,231,239,245,252,258,263,269,274,280,286,292,298,303,308,314,319,325,331,337,343,349,355,360,366,372,378,384,390,395,401,406,412,418,423,428,434,439,445,451,457,463,468,474,480,486,492,498,504,510,516,522,528,534,539,545,551,557,562,568,573,579,585,591,597,603,609,614,620,625,631,637,643,649,655,660,666,672,678,683,689,695,701,707,712,718,724,730,735,741,747,753,758,764,769,775,780,786],"code",{"__ignoreMap":227},[232,233,236],"span",{"class":234,"line":235},"line",1,[232,237,238],{},"Build the API key management system so external services can authenticate\n",[232,240,242],{"class":234,"line":241},2,[232,243,244],{},"with the app.\n",[232,246,248],{"class":234,"line":247},3,[232,249,251],{"emptyLinePlaceholder":250},true,"\n",[232,253,255],{"class":234,"line":254},4,[232,256,257],{},"Permissions:\n",[232,259,261],{"class":234,"line":260},5,[232,262,251],{"emptyLinePlaceholder":250},[232,264,266],{"class":234,"line":265},6,[232,267,268],{},"Add these keys to `shared\u002Fpermissions.ts` if they don't already exist:\n",[232,270,272],{"class":234,"line":271},7,[232,273,251],{"emptyLinePlaceholder":250},[232,275,277],{"class":234,"line":276},8,[232,278,279],{},"```\n",[232,281,283],{"class":234,"line":282},9,[232,284,285],{},"\"api_keys.view\": [\"owner\", \"admin\"],\n",[232,287,289],{"class":234,"line":288},10,[232,290,291],{},"\"api_keys.create\": [\"owner\", \"admin\"],\n",[232,293,295],{"class":234,"line":294},11,[232,296,297],{},"\"api_keys.revoke\": [\"owner\", \"admin\"],\n",[232,299,301],{"class":234,"line":300},12,[232,302,279],{},[232,304,306],{"class":234,"line":305},13,[232,307,251],{"emptyLinePlaceholder":250},[232,309,311],{"class":234,"line":310},14,[232,312,313],{},"Database (via Supabase MCP):\n",[232,315,317],{"class":234,"line":316},15,[232,318,251],{"emptyLinePlaceholder":250},[232,320,322],{"class":234,"line":321},16,[232,323,324],{},"Create an `api_keys` table — id (uuid, default gen_random_uuid(), primary\n",[232,326,328],{"class":234,"line":327},17,[232,329,330],{},"key), team_id (uuid, references teams(id) on delete cascade, not null),\n",[232,332,334],{"class":234,"line":333},18,[232,335,336],{},"created_by (uuid, references profiles(id), not null), name (text, not null),\n",[232,338,340],{"class":234,"line":339},19,[232,341,342],{},"key_prefix (text, not null), key_hash (text, not null), last_used_at\n",[232,344,346],{"class":234,"line":345},20,[232,347,348],{},"(timestamptz, nullable), expires_at (timestamptz, nullable), revoked_at\n",[232,350,352],{"class":234,"line":351},21,[232,353,354],{},"(timestamptz, nullable), created_at (timestamptz, default now()).\n",[232,356,358],{"class":234,"line":357},22,[232,359,251],{"emptyLinePlaceholder":250},[232,361,363],{"class":234,"line":362},23,[232,364,365],{},"Enable RLS. Add a SELECT-only policy using `is_team_member()` scoped to\n",[232,367,369],{"class":234,"line":368},24,[232,370,371],{},"`team_id` — follow the `announcements_team_member_read` pattern in\n",[232,373,375],{"class":234,"line":374},25,[232,376,377],{},"`supabase\u002Fmigrations\u002F00001_initial_schema.sql`. Writes go through the\n",[232,379,381],{"class":234,"line":380},26,[232,382,383],{},"service-role server routes below, so no insert\u002Fupdate\u002Fdelete RLS policies\n",[232,385,387],{"class":234,"line":386},27,[232,388,389],{},"are needed.\n",[232,391,393],{"class":234,"line":392},28,[232,394,251],{"emptyLinePlaceholder":250},[232,396,398],{"class":234,"line":397},29,[232,399,400],{},"Enable the activity log for the table, excluding the secret hash:\n",[232,402,404],{"class":234,"line":403},30,[232,405,251],{"emptyLinePlaceholder":250},[232,407,409],{"class":234,"line":408},31,[232,410,411],{},"```sql\n",[232,413,415],{"class":234,"line":414},32,[232,416,417],{},"select enable_activity_log('api_keys', exclude_cols => array['key_hash']);\n",[232,419,421],{"class":234,"line":420},33,[232,422,279],{},[232,424,426],{"class":234,"line":425},34,[232,427,251],{"emptyLinePlaceholder":250},[232,429,431],{"class":234,"line":430},35,[232,432,433],{},"Server routes (all use `authUser`):\n",[232,435,437],{"class":234,"line":436},36,[232,438,251],{"emptyLinePlaceholder":250},[232,440,442],{"class":234,"line":441},37,[232,443,444],{},"- `GET \u002Fapi\u002Fteams\u002F[teamId]\u002Fapi-keys` — uses\n",[232,446,448],{"class":234,"line":447},38,[232,449,450],{},"  `authUser(event, \"api_keys.view\")`. Returns all API keys for the team\n",[232,452,454],{"class":234,"line":453},39,[232,455,456],{},"  (NEVER return key_hash in the response), ordered by created_at desc.\n",[232,458,460],{"class":234,"line":459},40,[232,461,462],{},"  Include the creator's name by joining profiles.\n",[232,464,466],{"class":234,"line":465},41,[232,467,251],{"emptyLinePlaceholder":250},[232,469,471],{"class":234,"line":470},42,[232,472,473],{},"- `POST \u002Fapi\u002Fteams\u002F[teamId]\u002Fapi-keys` — uses\n",[232,475,477],{"class":234,"line":476},43,[232,478,479],{},"  `authUser(event, \"api_keys.create\")`. Reads { name } from the body.\n",[232,481,483],{"class":234,"line":482},44,[232,484,485],{},"  Generates a new API key:\n",[232,487,489],{"class":234,"line":488},45,[232,490,491],{},"  1. Generate 32 random bytes using Node's `crypto.randomBytes`\n",[232,493,495],{"class":234,"line":494},46,[232,496,497],{},"  2. Base64url encode the bytes to create the raw key string\n",[232,499,501],{"class":234,"line":500},47,[232,502,503],{},"  3. Create a prefix from the first 8 characters of the encoded key\n",[232,505,507],{"class":234,"line":506},48,[232,508,509],{},"  4. Hash the full key with SHA-256 using Node's `crypto.createHash`\n",[232,511,513],{"class":234,"line":512},49,[232,514,515],{},"  5. Convert the hash to a hex string\n",[232,517,519],{"class":234,"line":518},50,[232,520,521],{},"  6. Store team_id, name, key_prefix, key_hash, and created_by\n",[232,523,525],{"class":234,"line":524},51,[232,526,527],{},"  7. Return the full key in the response — this is the ONLY time it is\n",[232,529,531],{"class":234,"line":530},52,[232,532,533],{},"     ever visible. The server never stores the raw key.\n",[232,535,537],{"class":234,"line":536},53,[232,538,251],{"emptyLinePlaceholder":250},[232,540,542],{"class":234,"line":541},54,[232,543,544],{},"- `PATCH \u002Fapi\u002Fteams\u002F[teamId]\u002Fapi-keys\u002F[keyId]` — uses\n",[232,546,548],{"class":234,"line":547},55,[232,549,550],{},"  `authUser(event, \"api_keys.revoke\")`. Sets revoked_at to now. Validates\n",[232,552,554],{"class":234,"line":553},56,[232,555,556],{},"  the key belongs to the team and is not already revoked.\n",[232,558,560],{"class":234,"line":559},57,[232,561,251],{"emptyLinePlaceholder":250},[232,563,565],{"class":234,"line":564},58,[232,566,567],{},"UI:\n",[232,569,571],{"class":234,"line":570},59,[232,572,251],{"emptyLinePlaceholder":250},[232,574,576],{"class":234,"line":575},60,[232,577,578],{},"Add an \"API Keys\" tab to the settings page. Create\n",[232,580,582],{"class":234,"line":581},61,[232,583,584],{},"`app\u002Fpages\u002Fapp\u002Fsettings\u002Fapi-keys.vue` (settings uses a `settings.vue` parent\n",[232,586,588],{"class":234,"line":587},62,[232,589,590],{},"with file-based child routes) and register the tab in the `links` computed\n",[232,592,594],{"class":234,"line":593},63,[232,595,596],{},"in `app\u002Fpages\u002Fapp\u002Fsettings.vue`. Gate the tab with `can(\"api_keys.view\")` so\n",[232,598,600],{"class":234,"line":599},64,[232,601,602],{},"only owners\u002Fadmins see it. Use `definePageMeta({ middleware:\n",[232,604,606],{"class":234,"line":605},65,[232,607,608],{},"[requirePermission(\"api_keys.view\", \"\u002Fapp\u002Fsettings\")] })` on the page itself.\n",[232,610,612],{"class":234,"line":611},66,[232,613,251],{"emptyLinePlaceholder":250},[232,615,617],{"class":234,"line":616},67,[232,618,619],{},"The API Keys settings tab should:\n",[232,621,623],{"class":234,"line":622},68,[232,624,251],{"emptyLinePlaceholder":250},[232,626,628],{"class":234,"line":627},69,[232,629,630],{},"- List all keys in a table showing: name, prefix (displayed as\n",[232,632,634],{"class":234,"line":633},70,[232,635,636],{},"  `sk_...{prefix}`), created by (name), created date, last used date\n",[232,638,640],{"class":234,"line":639},71,[232,641,642],{},"  (or \"Never\"), and status. Status is a badge: \"Active\" (green) if not\n",[232,644,646],{"class":234,"line":645},72,[232,647,648],{},"  revoked and not expired, \"Revoked\" (red) if revoked_at is set,\n",[232,650,652],{"class":234,"line":651},73,[232,653,654],{},"  \"Expired\" (neutral) if expires_at is in the past.\n",[232,656,658],{"class":234,"line":657},74,[232,659,251],{"emptyLinePlaceholder":250},[232,661,663],{"class":234,"line":662},75,[232,664,665],{},"- A \"Create Key\" button at the top right, wrapped in\n",[232,667,669],{"class":234,"line":668},76,[232,670,671],{},"  `CanAccess permission=\"api_keys.create\"`. Opens a `UModal` with a\n",[232,673,675],{"class":234,"line":674},77,[232,676,677],{},"  single field: key name (required). Show loading on submit.\n",[232,679,681],{"class":234,"line":680},78,[232,682,251],{"emptyLinePlaceholder":250},[232,684,686],{"class":234,"line":685},79,[232,687,688],{},"- After creation, show a second modal displaying the full API key with\n",[232,690,692],{"class":234,"line":691},80,[232,693,694],{},"  a copy-to-clipboard button and a warning message: \"Copy this key now.\n",[232,696,698],{"class":234,"line":697},81,[232,699,700],{},"  You will not be able to see it again.\" The modal should only be\n",[232,702,704],{"class":234,"line":703},82,[232,705,706],{},"  dismissible after the user has seen the key.\n",[232,708,710],{"class":234,"line":709},83,[232,711,251],{"emptyLinePlaceholder":250},[232,713,715],{"class":234,"line":714},84,[232,716,717],{},"- Each active key row has a \"Revoke\" button wrapped in\n",[232,719,721],{"class":234,"line":720},85,[232,722,723],{},"  `CanAccess permission=\"api_keys.revoke\"`. Clicking it shows a\n",[232,725,727],{"class":234,"line":726},86,[232,728,729],{},"  confirmation modal before revoking. Show per-row loading state.\n",[232,731,733],{"class":234,"line":732},87,[232,734,251],{"emptyLinePlaceholder":250},[232,736,738],{"class":234,"line":737},88,[232,739,740],{},"- Show an empty state when no keys exist with a message like\n",[232,742,744],{"class":234,"line":743},89,[232,745,746],{},"  \"No API keys yet. Create one to allow external services to access\n",[232,748,750],{"class":234,"line":749},90,[232,751,752],{},"  your data.\"\n",[232,754,756],{"class":234,"line":755},91,[232,757,251],{"emptyLinePlaceholder":250},[232,759,761],{"class":234,"line":760},92,[232,762,763],{},"- Show `USkeleton` placeholders on initial load.\n",[232,765,767],{"class":234,"line":766},93,[232,768,251],{"emptyLinePlaceholder":250},[232,770,772],{"class":234,"line":771},94,[232,773,774],{},"Use the icon `i-solar-key-bold-duotone` for the settings tab entry.\n",[232,776,778],{"class":234,"line":777},95,[232,779,251],{"emptyLinePlaceholder":250},[232,781,783],{"class":234,"line":782},96,[232,784,785],{},"Regenerate TypeScript types via Supabase MCP and save to\n",[232,787,789],{"class":234,"line":788},97,[232,790,791],{},"`shared\u002Ftypes\u002Fdatabase.types.ts`.\n",[217,793,795],{"id":794},"external-api","External API",[222,797,799],{"className":224,"code":798,"language":226,"meta":227,"style":227},"Build the external API so tools like n8n, Zapier, or custom integrations\ncan interact with the app's data programmatically.\n\n1. Create `server\u002Futils\u002FauthApiKey.ts` with an `authApiKey` function that\n   takes an H3 event. It should:\n\n   - Read the `Authorization` header and extract the Bearer token — throw\n     a 401 error if the header is missing or not in `Bearer \u003Ctoken>` format\n   - Hash the token with SHA-256 using Node's `crypto.createHash` (same\n     approach as the key generation step) and convert to hex\n   - Look up the hash in the `api_keys` table using an untagged service\n     role client (`serverSupabaseServiceRole\u003CDatabase>(event)`)\n   - Throw 401 if not found\n   - Throw 401 if `revoked_at` is set (key has been revoked)\n   - Throw 401 if `expires_at` is set and is in the past (key has expired)\n   - Update `last_used_at` to now (fire and forget — do not await)\n   - Build an audited client with `createAuditedClient({ actorId:\n     apiKey.created_by, teamId: apiKey.team_id, source: \"api\" })` so every\n     write from an API-key request is attributed in the activity log the\n     same way browser writes are\n   - Return `{ apiKey, client }` where `client` is the audited client and\n     `apiKey` is the full row including team_id and created_by\n\n2. Create public API routes under `\u002Fapi\u002Fv1\u002F`. These routes do NOT use\n   `authUser` — they use `authApiKey` instead. All queries are scoped to\n   the API key's `team_id`.\n\n   Analyze the app's existing database tables (exclude system tables like\n   teams, profiles, members, invitations, api_keys, rate_limits, and\n   schema_migrations). For each domain table found, create:\n\n   - `GET \u002Fapi\u002Fv1\u002F{table}` — returns all records for the key's team,\n     ordered by created_at desc. Support `?limit=` and `?offset=` for\n     pagination (default limit 50, max 100).\n\n   - `POST \u002Fapi\u002Fv1\u002F{table}` — creates a record. Accept all non-system\n     columns from the body. Set team_id from the API key and created_by\n     from the API key's created_by. Validate that required columns (not\n     null without defaults) are present.\n\n   - `PATCH \u002Fapi\u002Fv1\u002F{table}\u002F[id]` — updates a record. Validate the record\n     belongs to the API key's team before updating.\n\n   Include relevant joins where foreign keys exist (e.g., if a table\n   references another by ID, include the referenced name in GET responses).\n\nBoth `authUser` (browser sessions) and `authApiKey` (API keys) return an\naudited Supabase client built with `createAuditedClient`. The only\ndifference is how the caller authenticates. This is what makes API keys\nuseful — external services can manage data without needing a browser\nsession, and every write still lands in `activity_log` with the key's\ncreator as the actor.\n\nError responses for the v1 routes should use a consistent JSON format:\n`{ \"error\": \"message\" }` with appropriate HTTP status codes (400 for\nvalidation errors, 401 for auth errors, 404 for not found).\n",[229,800,801,806,811,815,820,825,829,834,839,844,849,854,859,864,869,874,879,884,889,894,899,904,909,913,918,923,928,932,937,942,947,951,956,961,966,970,975,980,985,990,994,999,1004,1008,1013,1018,1022,1027,1032,1037,1042,1047,1052,1056,1061,1066],{"__ignoreMap":227},[232,802,803],{"class":234,"line":235},[232,804,805],{},"Build the external API so tools like n8n, Zapier, or custom integrations\n",[232,807,808],{"class":234,"line":241},[232,809,810],{},"can interact with the app's data programmatically.\n",[232,812,813],{"class":234,"line":247},[232,814,251],{"emptyLinePlaceholder":250},[232,816,817],{"class":234,"line":254},[232,818,819],{},"1. Create `server\u002Futils\u002FauthApiKey.ts` with an `authApiKey` function that\n",[232,821,822],{"class":234,"line":260},[232,823,824],{},"   takes an H3 event. It should:\n",[232,826,827],{"class":234,"line":265},[232,828,251],{"emptyLinePlaceholder":250},[232,830,831],{"class":234,"line":271},[232,832,833],{},"   - Read the `Authorization` header and extract the Bearer token — throw\n",[232,835,836],{"class":234,"line":276},[232,837,838],{},"     a 401 error if the header is missing or not in `Bearer \u003Ctoken>` format\n",[232,840,841],{"class":234,"line":282},[232,842,843],{},"   - Hash the token with SHA-256 using Node's `crypto.createHash` (same\n",[232,845,846],{"class":234,"line":288},[232,847,848],{},"     approach as the key generation step) and convert to hex\n",[232,850,851],{"class":234,"line":294},[232,852,853],{},"   - Look up the hash in the `api_keys` table using an untagged service\n",[232,855,856],{"class":234,"line":300},[232,857,858],{},"     role client (`serverSupabaseServiceRole\u003CDatabase>(event)`)\n",[232,860,861],{"class":234,"line":305},[232,862,863],{},"   - Throw 401 if not found\n",[232,865,866],{"class":234,"line":310},[232,867,868],{},"   - Throw 401 if `revoked_at` is set (key has been revoked)\n",[232,870,871],{"class":234,"line":316},[232,872,873],{},"   - Throw 401 if `expires_at` is set and is in the past (key has expired)\n",[232,875,876],{"class":234,"line":321},[232,877,878],{},"   - Update `last_used_at` to now (fire and forget — do not await)\n",[232,880,881],{"class":234,"line":327},[232,882,883],{},"   - Build an audited client with `createAuditedClient({ actorId:\n",[232,885,886],{"class":234,"line":333},[232,887,888],{},"     apiKey.created_by, teamId: apiKey.team_id, source: \"api\" })` so every\n",[232,890,891],{"class":234,"line":339},[232,892,893],{},"     write from an API-key request is attributed in the activity log the\n",[232,895,896],{"class":234,"line":345},[232,897,898],{},"     same way browser writes are\n",[232,900,901],{"class":234,"line":351},[232,902,903],{},"   - Return `{ apiKey, client }` where `client` is the audited client and\n",[232,905,906],{"class":234,"line":357},[232,907,908],{},"     `apiKey` is the full row including team_id and created_by\n",[232,910,911],{"class":234,"line":362},[232,912,251],{"emptyLinePlaceholder":250},[232,914,915],{"class":234,"line":368},[232,916,917],{},"2. Create public API routes under `\u002Fapi\u002Fv1\u002F`. These routes do NOT use\n",[232,919,920],{"class":234,"line":374},[232,921,922],{},"   `authUser` — they use `authApiKey` instead. All queries are scoped to\n",[232,924,925],{"class":234,"line":380},[232,926,927],{},"   the API key's `team_id`.\n",[232,929,930],{"class":234,"line":386},[232,931,251],{"emptyLinePlaceholder":250},[232,933,934],{"class":234,"line":392},[232,935,936],{},"   Analyze the app's existing database tables (exclude system tables like\n",[232,938,939],{"class":234,"line":397},[232,940,941],{},"   teams, profiles, members, invitations, api_keys, rate_limits, and\n",[232,943,944],{"class":234,"line":403},[232,945,946],{},"   schema_migrations). For each domain table found, create:\n",[232,948,949],{"class":234,"line":408},[232,950,251],{"emptyLinePlaceholder":250},[232,952,953],{"class":234,"line":414},[232,954,955],{},"   - `GET \u002Fapi\u002Fv1\u002F{table}` — returns all records for the key's team,\n",[232,957,958],{"class":234,"line":420},[232,959,960],{},"     ordered by created_at desc. Support `?limit=` and `?offset=` for\n",[232,962,963],{"class":234,"line":425},[232,964,965],{},"     pagination (default limit 50, max 100).\n",[232,967,968],{"class":234,"line":430},[232,969,251],{"emptyLinePlaceholder":250},[232,971,972],{"class":234,"line":436},[232,973,974],{},"   - `POST \u002Fapi\u002Fv1\u002F{table}` — creates a record. Accept all non-system\n",[232,976,977],{"class":234,"line":441},[232,978,979],{},"     columns from the body. Set team_id from the API key and created_by\n",[232,981,982],{"class":234,"line":447},[232,983,984],{},"     from the API key's created_by. Validate that required columns (not\n",[232,986,987],{"class":234,"line":453},[232,988,989],{},"     null without defaults) are present.\n",[232,991,992],{"class":234,"line":459},[232,993,251],{"emptyLinePlaceholder":250},[232,995,996],{"class":234,"line":465},[232,997,998],{},"   - `PATCH \u002Fapi\u002Fv1\u002F{table}\u002F[id]` — updates a record. Validate the record\n",[232,1000,1001],{"class":234,"line":470},[232,1002,1003],{},"     belongs to the API key's team before updating.\n",[232,1005,1006],{"class":234,"line":476},[232,1007,251],{"emptyLinePlaceholder":250},[232,1009,1010],{"class":234,"line":482},[232,1011,1012],{},"   Include relevant joins where foreign keys exist (e.g., if a table\n",[232,1014,1015],{"class":234,"line":488},[232,1016,1017],{},"   references another by ID, include the referenced name in GET responses).\n",[232,1019,1020],{"class":234,"line":494},[232,1021,251],{"emptyLinePlaceholder":250},[232,1023,1024],{"class":234,"line":500},[232,1025,1026],{},"Both `authUser` (browser sessions) and `authApiKey` (API keys) return an\n",[232,1028,1029],{"class":234,"line":506},[232,1030,1031],{},"audited Supabase client built with `createAuditedClient`. The only\n",[232,1033,1034],{"class":234,"line":512},[232,1035,1036],{},"difference is how the caller authenticates. This is what makes API keys\n",[232,1038,1039],{"class":234,"line":518},[232,1040,1041],{},"useful — external services can manage data without needing a browser\n",[232,1043,1044],{"class":234,"line":524},[232,1045,1046],{},"session, and every write still lands in `activity_log` with the key's\n",[232,1048,1049],{"class":234,"line":530},[232,1050,1051],{},"creator as the actor.\n",[232,1053,1054],{"class":234,"line":536},[232,1055,251],{"emptyLinePlaceholder":250},[232,1057,1058],{"class":234,"line":541},[232,1059,1060],{},"Error responses for the v1 routes should use a consistent JSON format:\n",[232,1062,1063],{"class":234,"line":547},[232,1064,1065],{},"`{ \"error\": \"message\" }` with appropriate HTTP status codes (400 for\n",[232,1067,1068],{"class":234,"line":553},[232,1069,1070],{},"validation errors, 401 for auth errors, 404 for not found).\n",[1072,1073,1074,1079,1082],"tip",{},[198,1075,1076],{},[201,1077,1078],{},"Testing with curl",[198,1080,1081],{},"Once built, you can test the external API with curl:",[222,1083,1087],{"className":1084,"code":1085,"language":1086,"meta":227,"style":227},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# List records\ncurl -H \"Authorization: Bearer YOUR_API_KEY\" http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fv1\u002Fyour-table\n\n# Create a record\ncurl -X POST -H \"Authorization: Bearer YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"name\": \"Example\", \"email\": \"test@example.com\"}' \\\n  http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fv1\u002Fyour-table\n","bash",[229,1088,1089,1095,1118,1122,1127,1149,1163,1179],{"__ignoreMap":227},[232,1090,1091],{"class":234,"line":235},[232,1092,1094],{"class":1093},"sHwdD","# List records\n",[232,1096,1097,1101,1105,1109,1112,1115],{"class":234,"line":241},[232,1098,1100],{"class":1099},"sBMFI","curl",[232,1102,1104],{"class":1103},"sfazB"," -H",[232,1106,1108],{"class":1107},"sMK4o"," \"",[232,1110,1111],{"class":1103},"Authorization: Bearer YOUR_API_KEY",[232,1113,1114],{"class":1107},"\"",[232,1116,1117],{"class":1103}," http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fv1\u002Fyour-table\n",[232,1119,1120],{"class":234,"line":247},[232,1121,251],{"emptyLinePlaceholder":250},[232,1123,1124],{"class":234,"line":254},[232,1125,1126],{"class":1093},"# Create a record\n",[232,1128,1129,1131,1134,1137,1139,1141,1143,1145],{"class":234,"line":260},[232,1130,1100],{"class":1099},[232,1132,1133],{"class":1103}," -X",[232,1135,1136],{"class":1103}," POST",[232,1138,1104],{"class":1103},[232,1140,1108],{"class":1107},[232,1142,1111],{"class":1103},[232,1144,1114],{"class":1107},[232,1146,1148],{"class":1147},"sTEyZ"," \\\n",[232,1150,1151,1154,1156,1159,1161],{"class":234,"line":265},[232,1152,1153],{"class":1103},"  -H",[232,1155,1108],{"class":1107},[232,1157,1158],{"class":1103},"Content-Type: application\u002Fjson",[232,1160,1114],{"class":1107},[232,1162,1148],{"class":1147},[232,1164,1165,1168,1171,1174,1177],{"class":234,"line":271},[232,1166,1167],{"class":1103},"  -d",[232,1169,1170],{"class":1107}," '",[232,1172,1173],{"class":1103},"{\"name\": \"Example\", \"email\": \"test@example.com\"}",[232,1175,1176],{"class":1107},"'",[232,1178,1148],{"class":1147},[232,1180,1181],{"class":234,"line":276},[232,1182,1183],{"class":1103},"  http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fv1\u002Fyour-table\n",[1185,1186,1187],"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 .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 .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 pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}",{"title":227,"searchDepth":235,"depth":241,"links":1189},[],"Create the api_keys table, key management UI, and public API routes for external integrations","md",null,{},{"icon":62},{"title":59,"description":1190},"bjWGkOeF1vrVSZw_xAqggBwu-2-jO6njel4Lqy1zCdw",[1198,1200],{"title":52,"path":53,"stem":54,"description":1199,"icon":57,"children":-1},"How to install plugins into your VueStarter project.",{"title":64,"path":65,"stem":66,"description":1201,"icon":67,"children":-1},"Scheduled tasks that run on a recurring basis.",1777092169440]