-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpasskey-sql-auth.html
More file actions
647 lines (645 loc) · 277 KB
/
Copy pathpasskey-sql-auth.html
File metadata and controls
647 lines (645 loc) · 277 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
<!DOCTYPE html>
<html lang="en-US" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Implementing WebAuthn Passkeys with Pure SQL and NpgsqlRest | NpgsqlRest</title>
<meta name="description" content="Learn how to implement passwordless WebAuthn passkey authentication entirely in PostgreSQL using NpgsqlRest. Full SQL-based passkey registration, login, and credential management.">
<meta name="generator" content="VitePress v1.6.4">
<link rel="preload stylesheet" href="/assets/style.LpuNvTwL.css" as="style">
<link rel="preload stylesheet" href="/vp-icons.css" as="style">
<script type="module" src="/assets/app.VN9qkxsN.js"></script>
<link rel="preload" href="/assets/inter-roman-latin.Di8DUHzh.woff2" as="font" type="font/woff2" crossorigin="">
<link rel="modulepreload" href="/assets/chunks/theme.CbCjZi0Y.js">
<link rel="modulepreload" href="/assets/chunks/framework.CgT1UzWm.js">
<link rel="modulepreload" href="/assets/blog_passkey-sql-auth.md.BRkr8TRo.lean.js">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="shortcut icon" href="/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.ico">
<meta property="og:url" content="https://npgsqlrest.github.io">
<script defer data-id="101498393" src="//static.getclicky.com/js"></script>
<script id="check-dark-mode">(()=>{const e=localStorage.getItem("vitepress-theme-appearance")||"auto",a=window.matchMedia("(prefers-color-scheme: dark)").matches;(!e||e==="auto"?a:e==="dark")&&document.documentElement.classList.add("dark")})();</script>
<script id="check-mac-os">document.documentElement.classList.toggle("mac",/Mac|iPhone|iPod|iPad/i.test(navigator.platform));</script>
<meta name="keywords" content="webauthn passkey postgresql, passwordless authentication sql, passkey implementation postgres, npgsqlrest webauthn, sql authentication passkey, fido2 postgresql">
<meta property="og:title" content="Implementing WebAuthn Passkeys with Pure SQL and NpgsqlRest">
<meta property="og:description" content="Learn how to implement passwordless WebAuthn passkey authentication entirely in PostgreSQL using NpgsqlRest.">
<meta property="og:type" content="article">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Implementing WebAuthn Passkeys with Pure SQL and NpgsqlRest">
<meta name="twitter:description" content="Passwordless WebAuthn passkey authentication implemented entirely in PostgreSQL with NpgsqlRest.">
</head>
<body>
<div id="app"><div class="Layout" data-v-5d98c3a5><!--[--><!--]--><!--[--><span tabindex="-1" data-v-0b0ada53></span><a href="#VPContent" class="VPSkipLink visually-hidden" data-v-0b0ada53>Skip to content</a><!--]--><!----><header class="VPNav" data-v-5d98c3a5 data-v-ae24b3ad><div class="VPNavBar" data-v-ae24b3ad data-v-6aa21345><div class="wrapper" data-v-6aa21345><div class="container" data-v-6aa21345><div class="title" data-v-6aa21345><div class="VPNavBarTitle has-sidebar" data-v-6aa21345 data-v-1168a8e4><a class="title" href="/" data-v-1168a8e4><!--[--><!--]--><!--[--><img class="VPImage logo" src="/favicon.ico" alt data-v-8426fc1a><!--]--><span data-v-1168a8e4>NpgsqlRest</span><!--[--><!--]--></a></div></div><div class="content" data-v-6aa21345><div class="content-body" data-v-6aa21345><!--[--><!--]--><div class="VPNavBarSearch search" data-v-6aa21345><!--[--><!----><div id="local-search"><button type="button" class="DocSearch DocSearch-Button" aria-label="Search"><span class="DocSearch-Button-Container"><span class="vp-icon DocSearch-Search-Icon"></span><span class="DocSearch-Button-Placeholder">Search</span></span><span class="DocSearch-Button-Keys"><kbd class="DocSearch-Button-Key"></kbd><kbd class="DocSearch-Button-Key">K</kbd></span></button></div><!--]--></div><nav aria-labelledby="main-nav-aria-label" class="VPNavBarMenu menu" data-v-6aa21345 data-v-dc692963><span id="main-nav-aria-label" class="visually-hidden" data-v-dc692963> Main Navigation </span><!--[--><!--[--><div class="VPFlyout VPNavBarMenuGroup" data-v-dc692963 data-v-cf11d7a2><button type="button" class="button" aria-haspopup="true" aria-expanded="false" data-v-cf11d7a2><span class="text" data-v-cf11d7a2><!----><span data-v-cf11d7a2>Guide</span><span class="vpi-chevron-down text-icon" data-v-cf11d7a2></span></span></button><div class="menu" data-v-cf11d7a2><div class="VPMenu" data-v-cf11d7a2 data-v-b98bc113><div class="items" data-v-b98bc113><!--[--><!--[--><div class="VPMenuGroup" data-v-b98bc113 data-v-69e747b5><p class="title" data-v-69e747b5>Getting Started</p><!--[--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/" data-v-35975db6><!--[--><span data-v-35975db6>Overview</span><!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/installation.html" data-v-35975db6><!--[--><span data-v-35975db6>Installation</span><!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/quick-start.html" data-v-35975db6><!--[--><span data-v-35975db6>Quick Start</span><!--]--></a></div><!--]--><!--]--></div><!--]--><!--[--><div class="VPMenuGroup" data-v-b98bc113 data-v-69e747b5><p class="title" data-v-69e747b5>Topics</p><!--[--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/sql-files.html" data-v-35975db6><!--[--><span data-v-35975db6>SQL File Endpoints</span><!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/configuration.html" data-v-35975db6><!--[--><span data-v-35975db6>Configuration Guide</span><!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/annotations.html" data-v-35975db6><!--[--><span data-v-35975db6>Comment Annotations Guide</span><!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/authentication.html" data-v-35975db6><!--[--><span data-v-35975db6>Authentication</span><!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/sse.html" data-v-35975db6><!--[--><span data-v-35975db6>Server-Sent Events</span><!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/http-types.html" data-v-35975db6><!--[--><span data-v-35975db6>HTTP Custom Types</span><!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/proxy.html" data-v-35975db6><!--[--><span data-v-35975db6>Proxy Endpoints</span><!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/testing.html" data-v-35975db6><!--[--><span data-v-35975db6>Testing</span><!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/logging.html" data-v-35975db6><!--[--><span data-v-35975db6>Logging</span><!--]--></a></div><!--]--><!--]--></div><!--]--><!--[--><div class="VPMenuGroup" data-v-b98bc113 data-v-69e747b5><p class="title" data-v-69e747b5>Reference</p><!--[--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/faq.html" data-v-35975db6><!--[--><span data-v-35975db6>FAQ & Troubleshooting</span><!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-35975db6><a class="VPLink link" href="/guide/changelog/" data-v-35975db6><!--[--><span data-v-35975db6>Changelog</span><!--]--></a></div><!--]--><!--]--></div><!--]--><!--]--></div><!--[--><!--]--></div></div></div><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/examples/" tabindex="0" data-v-dc692963 data-v-e56f3d57><!--[--><span data-v-e56f3d57>Examples</span><!--]--></a><!--]--><!--[--><div class="VPFlyout VPNavBarMenuGroup" data-v-dc692963 data-v-cf11d7a2><button type="button" class="button" aria-haspopup="true" aria-expanded="false" data-v-cf11d7a2><span class="text" data-v-cf11d7a2><!----><span data-v-cf11d7a2>Reference</span><span class="vpi-chevron-down text-icon" data-v-cf11d7a2></span></span></button><div class="menu" data-v-cf11d7a2><div class="VPMenu" data-v-cf11d7a2 data-v-b98bc113><div class="items" data-v-b98bc113><!--[--><!--[--><div class="VPMenuLink" data-v-b98bc113 data-v-35975db6><a class="VPLink link" href="/annotations/" data-v-35975db6><!--[--><span data-v-35975db6>Annotations</span><!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-b98bc113 data-v-35975db6><a class="VPLink link" href="/config/" data-v-35975db6><!--[--><span data-v-35975db6>Configuration</span><!--]--></a></div><!--]--><!--]--></div><!--[--><!--]--></div></div></div><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/blog/" tabindex="0" data-v-dc692963 data-v-e56f3d57><!--[--><span data-v-e56f3d57>Blog</span><!--]--></a><!--]--><!--]--></nav><!----><div class="VPNavBarAppearance appearance" data-v-6aa21345 data-v-6c893767><button class="VPSwitch VPSwitchAppearance" type="button" role="switch" title aria-checked="false" data-v-6c893767 data-v-5337faa4 data-v-1d5665e3><span class="check" data-v-1d5665e3><span class="icon" data-v-1d5665e3><!--[--><span class="vpi-sun sun" data-v-5337faa4></span><span class="vpi-moon moon" data-v-5337faa4></span><!--]--></span></span></button></div><div class="VPSocialLinks VPNavBarSocialLinks social-links" data-v-6aa21345 data-v-0394ad82 data-v-7bc22406><!--[--><a class="VPSocialLink no-icon" href="https://github.com/NpgsqlRest/NpgsqlRest" aria-label="github" target="_blank" rel="noopener" data-v-7bc22406 data-v-bd121fe5><span class="vpi-social-github"></span></a><a class="VPSocialLink no-icon" href="https://patreon.com/vbconsulting" aria-label="Patreon" target="_blank" rel="noopener" data-v-7bc22406 data-v-bd121fe5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 7.463 3.097 9.746 2.602 2.124 6.19 2.078 9.402 1.836 1.883-.142 3.939-.358 5.477-1.528 1.564-1.19 1.873-3.298 1.972-5.151.134-2.51.187-5.016-.037-7.233zm-2.55 7.703c-.078 1.593-.383 3.473-1.795 4.334-1.351.824-3.151.973-4.686 1.083-2.891.207-6.063.276-8.514-1.399C3.106 17.39 2.924 14.34 2.95 11.67c.03-3.206.678-6.721 3.654-8.375 2.756-1.531 6.556-1.727 9.544-.872 2.339.669 4.351 2.678 4.355 5.217.007 2.583-.016 5.168-.095 7.273z"/><path d="M16.088 5.19c-1.008-.542-2.607-.48-3.441.388-.568.591-.773 1.389-.8 2.172-.034.993.068 2.013.361 2.953.307.985 1.034 1.848 2.077 2.012.878.138 1.83-.088 2.458-.732.7-.718.88-1.81.877-2.779-.003-.989-.084-1.99-.423-2.91-.268-.726-.654-1.328-1.109-1.104zm-.294 5.482c-.182.462-.581.771-1.091.696-.532-.078-.859-.533-1.04-1.005-.248-.647-.327-1.375-.312-2.06.01-.46.095-.939.371-1.313.345-.467.993-.447 1.426-.115.41.315.607.835.74 1.32.18.66.211 1.675-.094 2.477zM8.4 10.394c.233 1.084.755 2.299 1.907 2.579.934.227 1.993-.165 2.434-1.048.455-.91.346-1.995.166-2.972-.165-.893-.46-1.885-1.207-2.445-.745-.559-1.832-.457-2.479.192-.84.843-.958 2.138-.955 3.277.001.147.119.364.134.417zm1.363-2.808c.302-.321.805-.336 1.14-.054.445.375.618 1.015.75 1.558.131.54.212 1.156.082 1.703-.1.423-.378.821-.84.882-.469.062-.886-.306-1.088-.704-.326-.644-.401-1.426-.381-2.148.013-.465.104-.993.337-1.237z"/></svg></a><a class="VPSocialLink no-icon" href="https://buymeacoffee.com/vbilopavu" aria-label="Buy Me a Coffee" target="_blank" rel="noopener" data-v-7bc22406 data-v-bd121fe5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364z"/></svg></a><!--]--></div><div class="VPFlyout VPNavBarExtra extra" data-v-6aa21345 data-v-bb2aa2f0 data-v-cf11d7a2><button type="button" class="button" aria-haspopup="true" aria-expanded="false" aria-label="extra navigation" data-v-cf11d7a2><span class="vpi-more-horizontal icon" data-v-cf11d7a2></span></button><div class="menu" data-v-cf11d7a2><div class="VPMenu" data-v-cf11d7a2 data-v-b98bc113><!----><!--[--><!--[--><!----><div class="group" data-v-bb2aa2f0><div class="item appearance" data-v-bb2aa2f0><p class="label" data-v-bb2aa2f0>Appearance</p><div class="appearance-action" data-v-bb2aa2f0><button class="VPSwitch VPSwitchAppearance" type="button" role="switch" title aria-checked="false" data-v-bb2aa2f0 data-v-5337faa4 data-v-1d5665e3><span class="check" data-v-1d5665e3><span class="icon" data-v-1d5665e3><!--[--><span class="vpi-sun sun" data-v-5337faa4></span><span class="vpi-moon moon" data-v-5337faa4></span><!--]--></span></span></button></div></div></div><div class="group" data-v-bb2aa2f0><div class="item social-links" data-v-bb2aa2f0><div class="VPSocialLinks social-links-list" data-v-bb2aa2f0 data-v-7bc22406><!--[--><a class="VPSocialLink no-icon" href="https://github.com/NpgsqlRest/NpgsqlRest" aria-label="github" target="_blank" rel="noopener" data-v-7bc22406 data-v-bd121fe5><span class="vpi-social-github"></span></a><a class="VPSocialLink no-icon" href="https://patreon.com/vbconsulting" aria-label="Patreon" target="_blank" rel="noopener" data-v-7bc22406 data-v-bd121fe5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 7.463 3.097 9.746 2.602 2.124 6.19 2.078 9.402 1.836 1.883-.142 3.939-.358 5.477-1.528 1.564-1.19 1.873-3.298 1.972-5.151.134-2.51.187-5.016-.037-7.233zm-2.55 7.703c-.078 1.593-.383 3.473-1.795 4.334-1.351.824-3.151.973-4.686 1.083-2.891.207-6.063.276-8.514-1.399C3.106 17.39 2.924 14.34 2.95 11.67c.03-3.206.678-6.721 3.654-8.375 2.756-1.531 6.556-1.727 9.544-.872 2.339.669 4.351 2.678 4.355 5.217.007 2.583-.016 5.168-.095 7.273z"/><path d="M16.088 5.19c-1.008-.542-2.607-.48-3.441.388-.568.591-.773 1.389-.8 2.172-.034.993.068 2.013.361 2.953.307.985 1.034 1.848 2.077 2.012.878.138 1.83-.088 2.458-.732.7-.718.88-1.81.877-2.779-.003-.989-.084-1.99-.423-2.91-.268-.726-.654-1.328-1.109-1.104zm-.294 5.482c-.182.462-.581.771-1.091.696-.532-.078-.859-.533-1.04-1.005-.248-.647-.327-1.375-.312-2.06.01-.46.095-.939.371-1.313.345-.467.993-.447 1.426-.115.41.315.607.835.74 1.32.18.66.211 1.675-.094 2.477zM8.4 10.394c.233 1.084.755 2.299 1.907 2.579.934.227 1.993-.165 2.434-1.048.455-.91.346-1.995.166-2.972-.165-.893-.46-1.885-1.207-2.445-.745-.559-1.832-.457-2.479.192-.84.843-.958 2.138-.955 3.277.001.147.119.364.134.417zm1.363-2.808c.302-.321.805-.336 1.14-.054.445.375.618 1.015.75 1.558.131.54.212 1.156.082 1.703-.1.423-.378.821-.84.882-.469.062-.886-.306-1.088-.704-.326-.644-.401-1.426-.381-2.148.013-.465.104-.993.337-1.237z"/></svg></a><a class="VPSocialLink no-icon" href="https://buymeacoffee.com/vbilopavu" aria-label="Buy Me a Coffee" target="_blank" rel="noopener" data-v-7bc22406 data-v-bd121fe5><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364z"/></svg></a><!--]--></div></div></div><!--]--><!--]--></div></div></div><!--[--><!--]--><button type="button" class="VPNavBarHamburger hamburger" aria-label="mobile navigation" aria-expanded="false" aria-controls="VPNavScreen" data-v-6aa21345 data-v-e5dd9c1c><span class="container" data-v-e5dd9c1c><span class="top" data-v-e5dd9c1c></span><span class="middle" data-v-e5dd9c1c></span><span class="bottom" data-v-e5dd9c1c></span></span></button></div></div></div></div><div class="divider" data-v-6aa21345><div class="divider-line" data-v-6aa21345></div></div></div><!----></header><div class="VPLocalNav has-sidebar empty" data-v-5d98c3a5 data-v-a6f0e41e><div class="container" data-v-a6f0e41e><button class="menu" aria-expanded="false" aria-controls="VPSidebarNav" data-v-a6f0e41e><span class="vpi-align-left menu-icon" data-v-a6f0e41e></span><span class="menu-text" data-v-a6f0e41e>Menu</span></button><div class="VPLocalNavOutlineDropdown" style="--vp-vh:0px;" data-v-a6f0e41e data-v-8a42e2b4><button data-v-8a42e2b4>Return to top</button><!----></div></div></div><aside class="VPSidebar" data-v-5d98c3a5 data-v-319d5ca6><div class="curtain" data-v-319d5ca6></div><nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1" data-v-319d5ca6><span class="visually-hidden" id="sidebar-aria-label" data-v-319d5ca6> Sidebar Navigation </span><!--[--><!--]--><!--[--><div class="no-transition group" data-v-c40bc020><section class="VPSidebarItem level-0 collapsible has-active" data-v-c40bc020 data-v-b3fd67f8><div class="item" role="button" tabindex="0" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><h2 class="text" data-v-b3fd67f8>Tutorials</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-b3fd67f8><span class="vpi-chevron-right caret-icon" data-v-b3fd67f8></span></div></div><div class="items" data-v-b3fd67f8><!--[--><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/npgsqlrest-3.19-sql-test-runner-watch-mode.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Tests Are SQL Files Too</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/case-study-zero-backend-code.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Case Study: 74 Endpoints, Zero Backend Code</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/typescript-codegen-walkthrough.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>TypeScript Code Generation Walkthrough</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/npgsqlrest-3.13-production-patterns.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>NpgsqlRest 3.13.0: Production Patterns</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/sql-rest-api.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>SQL REST API</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/sql-file-source-rest-api-from-plain-sql.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>SQL File Source</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/database-level-security-postgresql-authentication.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Database-Level Security</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/multiple-auth-schemes-rbac-external-providers.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Multiple Auth Schemes & RBAC</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/passkey-sql-auth.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Passkey SQL Auth</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/end-to-end-static-type-checking-postgresql-typescript.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>End-to-End Type Checking</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/real-time-chat-postgresql-sse-npgsqlrest.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Real-Time Chat with SSE</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/external-api-calls-postgresql-http-types.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>External API Calls</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/web-scraping-postgresql-http-types-xml.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Web Scraping with HTTP Types</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/reverse-proxy-postgresql-ai-service-npgsqlrest.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Reverse Proxy & AI Service</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/secure-image-uploads-postgresql-typescript.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Secure Image Uploads</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/csv-excel-ingestion-postgresql-npgsqlrest.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>CSV & Excel Ingestion</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/postgresql-bi-server-excel-csv-basic-auth.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>PostgreSQL BI Server</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-c40bc020><section class="VPSidebarItem level-0 collapsible" data-v-c40bc020 data-v-b3fd67f8><div class="item" role="button" tabindex="0" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><h2 class="text" data-v-b3fd67f8>Feature Deep Dives</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-b3fd67f8><span class="vpi-chevron-right caret-icon" data-v-b3fd67f8></span></div></div><div class="items" data-v-b3fd67f8><!--[--><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/excel-export-table-format-postgresql-npgsqlrest.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Excel Exports Done Right</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/custom-types-multiset-rest-api.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Custom Types & Multiset</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/performance-scalability-high-availability-npgsqlrest.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Performance & High Availability</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/what-have-stored-procedures-ever-done-for-us.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>What Have Stored Procedures Done for Us?</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/the-power-of-simplicity.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>The Power of Simplicity</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-c40bc020><section class="VPSidebarItem level-0 collapsible" data-v-c40bc020 data-v-b3fd67f8><div class="item" role="button" tabindex="0" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><h2 class="text" data-v-b3fd67f8>Benchmarks & Comparisons</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-b3fd67f8><span class="vpi-chevron-right caret-icon" data-v-b3fd67f8></span></div></div><div class="items" data-v-b3fd67f8><!--[--><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/postgresql-rest-api-benchmark-2026.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Benchmark 2026</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/blog/npgsqlrest-vs-postgrest-supabase-comparison.html" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>NpgsqlRest vs PostgREST vs Supabase</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-c40bc020><section class="VPSidebarItem level-0 collapsible" data-v-c40bc020 data-v-b3fd67f8><div class="item" role="button" tabindex="0" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><h2 class="text" data-v-b3fd67f8>Other Links</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-b3fd67f8><span class="vpi-chevron-right caret-icon" data-v-b3fd67f8></span></div></div><div class="items" data-v-b3fd67f8><!--[--><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/guide/" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Guide</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/annotations/" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Annotations Reference</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-b3fd67f8 data-v-b3fd67f8><div class="item" data-v-b3fd67f8><div class="indicator" data-v-b3fd67f8></div><a class="VPLink link link" href="/config/" data-v-b3fd67f8><!--[--><p class="text" data-v-b3fd67f8>Configuration Reference</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><!--]--><!--[--><!--]--></nav></aside><div class="VPContent has-sidebar" id="VPContent" data-v-5d98c3a5 data-v-1428d186><div class="VPDoc has-sidebar has-aside" data-v-1428d186 data-v-39a288b8><!--[--><!--]--><div class="container" data-v-39a288b8><div class="aside" data-v-39a288b8><div class="aside-curtain" data-v-39a288b8></div><div class="aside-container" data-v-39a288b8><div class="aside-content" data-v-39a288b8><div class="VPDocAside" data-v-39a288b8 data-v-3f215769><!--[--><!--]--><!--[--><!--]--><nav aria-labelledby="doc-outline-aria-label" class="VPDocAsideOutline" data-v-3f215769 data-v-a5bbad30><div class="content" data-v-a5bbad30><div class="outline-marker" data-v-a5bbad30></div><div aria-level="2" class="outline-title" id="doc-outline-aria-label" role="heading" data-v-a5bbad30>On this page</div><ul class="VPDocOutlineItem root" data-v-a5bbad30 data-v-b933a997><!--[--><!--]--></ul></div></nav><!--[--><!--[--><!--[--><!--[--><div class="comments-outline-link" data-v-6357a5e2><a href="#comments" class="outline-link" data-v-6357a5e2>Comments</a></div><div class="blog-sidebar" data-v-0f020bdc><div class="blog-sidebar-title" data-v-0f020bdc>More Blog Posts</div><nav class="blog-sidebar-nav" data-v-0f020bdc><!--[--><a href="/blog/the-backend-that-writes-itself-presentation" class="blog-sidebar-link" data-v-0f020bdc>The Backend That Writes Itself (Slide Deck)</a><a href="/blog/case-study-zero-backend-code" class="blog-sidebar-link" data-v-0f020bdc>Case Study: 74 Endpoints, Zero Backend Code</a><a href="/blog/typescript-codegen-walkthrough" class="blog-sidebar-link" data-v-0f020bdc>TypeScript Code Generation Walkthrough</a><a href="/blog/npgsqlrest-3.13-production-patterns" class="blog-sidebar-link" data-v-0f020bdc>NpgsqlRest 3.13.0: Production Patterns</a><a href="/blog/sql-rest-api" class="blog-sidebar-link" data-v-0f020bdc>SQL REST API</a><a href="/blog/excel-export-table-format-postgresql-npgsqlrest" class="blog-sidebar-link" data-v-0f020bdc>Excel Exports Done Right</a><a href="/blog/custom-types-multiset-rest-api" class="blog-sidebar-link" data-v-0f020bdc>Custom Types & Multiset</a><a href="/blog/performance-scalability-high-availability-npgsqlrest" class="blog-sidebar-link" data-v-0f020bdc>Performance & High Availability</a><a href="/blog/postgresql-rest-api-benchmark-2026" class="blog-sidebar-link" data-v-0f020bdc>Benchmark 2026</a><a href="/blog/end-to-end-static-type-checking-postgresql-typescript" class="blog-sidebar-link" data-v-0f020bdc>End-to-End Type Checking</a><a href="/blog/database-level-security-postgresql-authentication" class="blog-sidebar-link" data-v-0f020bdc>Database-Level Security</a><a href="/blog/multiple-auth-schemes-rbac-external-providers" class="blog-sidebar-link" data-v-0f020bdc>Multiple Auth Schemes & RBAC</a><a href="/blog/postgresql-bi-server-excel-csv-basic-auth" class="blog-sidebar-link" data-v-0f020bdc>PostgreSQL BI Server</a><a href="/blog/secure-image-uploads-postgresql-typescript" class="blog-sidebar-link" data-v-0f020bdc>Secure Image Uploads</a><a href="/blog/csv-excel-ingestion-postgresql-npgsqlrest" class="blog-sidebar-link" data-v-0f020bdc>CSV & Excel Ingestion</a><a href="/blog/real-time-chat-postgresql-sse-npgsqlrest" class="blog-sidebar-link" data-v-0f020bdc>Real-Time Chat with SSE</a><a href="/blog/external-api-calls-postgresql-http-types" class="blog-sidebar-link" data-v-0f020bdc>External API Calls</a><a href="/blog/reverse-proxy-postgresql-ai-service-npgsqlrest" class="blog-sidebar-link" data-v-0f020bdc>Reverse Proxy & AI Service</a><a href="/blog/npgsqlrest-vs-postgrest-supabase-comparison" class="blog-sidebar-link" data-v-0f020bdc>NpgsqlRest vs PostgREST vs Supabase</a><a href="/blog/what-have-stored-procedures-ever-done-for-us" class="blog-sidebar-link" data-v-0f020bdc>What Have Stored Procedures Done for Us?</a><!--]--></nav></div><!--]--><!--]--><!--]--><!--]--><div class="spacer" data-v-3f215769></div><!--[--><!--]--><!----><!--[--><!--]--><!--[--><!--]--></div></div></div></div><div class="content" data-v-39a288b8><div class="content-container" data-v-39a288b8><!--[--><!--]--><main class="main" data-v-39a288b8><div style="position:relative;" class="vp-doc _blog_passkey-sql-auth" data-v-39a288b8><div><h1 id="implementing-webauthn-passkeys-with-pure-sql-and-npgsqlrest" tabindex="-1">Implementing WebAuthn Passkeys with Pure SQL and NpgsqlRest <a class="header-anchor" href="#implementing-webauthn-passkeys-with-pure-sql-and-npgsqlrest" aria-label="Permalink to "Implementing WebAuthn Passkeys with Pure SQL and NpgsqlRest""></a></h1><p class="blog-meta"><span class="tag">WebAuthn</span> · <span class="tag">Authentication</span> · <span class="tag">January 2026</span></p><hr><p>WebAuthn passkeys replace the weakest part of most systems - the password - with public-key cryptography tied to biometric or PIN verification on the user's device. There is no shared secret to phish, reuse across sites, or leak in a database breach.</p><p>NpgsqlRest now supports <strong>built-in passkey authentication</strong> that lets you implement passwordless login with your authentication logic written entirely in PostgreSQL functions. No external authentication libraries, no third-party services—just your database, a few SQL functions, and a client-side script to call the browser's WebAuthn API (see the <a href="https://github.com/NpgsqlRest/npgsqlrest-docs/blob/main/examples/13_passkey/src/passkey.ts" target="_blank" rel="noreferrer">passkey.ts example</a> you can use as a starting point).</p><h2 id="what-gets-stored-and-what-doesn-t" tabindex="-1">What Gets Stored (And What Doesn't) <a class="header-anchor" href="#what-gets-stored-and-what-doesn-t" aria-label="Permalink to "What Gets Stored (And What Doesn't)""></a></h2><p>A common misconception about passkeys is that they store biometric data. <strong>They don't.</strong> Here's what actually happens:</p><ol><li><strong>During registration</strong>, the user's device generates a public/private key pair. The private key never leaves the device and is protected by biometrics or a PIN</li><li><strong>Your database stores only the public key</strong>, a credential ID, and metadata like the signature counter</li><li><strong>During login</strong>, the device signs a challenge with the private key, and your server verifies it with the stored public key</li></ol><p>No fingerprints, no facial recognition data, no personal biometric information ever touches your server. You're storing cryptographic keys, not sensitive personal data.</p><h2 id="architecture-overview" tabindex="-1">Architecture Overview <a class="header-anchor" href="#architecture-overview" aria-label="Permalink to "Architecture Overview""></a></h2><p>NpgsqlRest's passkey implementation follows the WebAuthn specification with a SQL-first approach:</p><div class="language-mermaid vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">mermaid</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">flowchart LR</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> A["Browser</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (Client Script)"] <--> B["NpgsqlRest</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (Endpoints + CBOR)"] <--> C["PostgreSQL</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (SQL Functions)"]</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div><ul><li><strong>Browser</strong>: A client script that calls the browser's WebAuthn API and communicates with NpgsqlRest endpoints. See the <a href="https://github.com/NpgsqlRest/npgsqlrest-docs/blob/main/examples/13_passkey/src/passkey.ts" target="_blank" rel="noreferrer">passkey.ts example</a> for a complete TypeScript implementation.</li><li><strong>NpgsqlRest</strong>: Provides the HTTP endpoints and handles CBOR parsing/verification.</li><li><strong>PostgreSQL</strong>: Your SQL functions control the entire authentication flow—challenge creation, user management, credential storage.</li></ul><p>The division of labor: NpgsqlRest does the cryptographic work (parsing CBOR attestation objects, verifying signatures with EC and RSA keys) while your SQL functions define the business logic.</p><h2 id="complete-example-walkthrough" tabindex="-1">Complete Example Walkthrough <a class="header-anchor" href="#complete-example-walkthrough" aria-label="Permalink to "Complete Example Walkthrough""></a></h2><p>The full source for the example below is in the <a href="https://github.com/NpgsqlRest/npgsqlrest-docs/tree/main/examples/13_passkey" target="_blank" rel="noreferrer">examples/13_passkey</a> directory.</p><h3 id="_1-database-schema" tabindex="-1">1. Database Schema <a class="header-anchor" href="#_1-database-schema" aria-label="Permalink to "1. Database Schema""></a></h3><p>First, create the tables to store users, passkeys, and challenges:</p><details class="code-collapsible" open><summary>sql</summary><div class="language-sql vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;">-- Users table (can integrate with existing users)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">create</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> table</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> users</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">serial</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> primary key</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> username </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> not null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> email </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> password</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> text</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;">-- null for passkey-only users</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> created_at </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">timestamptz</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> default</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> now</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">()</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">);</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">create</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> unique index</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> on</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> users(username);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;">-- Passkeys table - stores the public keys</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">create</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> table</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> passkeys</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> credential_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> primary key</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> not null</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> references</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> users(user_id) </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">on delete cascade</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_handle </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> unique</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> not null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> public_key </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> not null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> public_key_algorithm </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> not null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> sign_count </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> not null</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> default</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> 0</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> transports </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">[],</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> backup_eligible </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">boolean</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> default</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> false,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> device_name </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> created_at </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">timestamptz</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> default</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> now</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">()</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> last_used_at </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">timestamptz</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;">-- Challenge storage for replay protection</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">create</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> table</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> passkey_challenges</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> not null</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> generated</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> always</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> as</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> identity</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> primary key</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> challenge </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> not null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;">-- null for authentication, set for registration</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> operation </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> not null</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> check</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (operation </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">in</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'registration'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'authentication'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)),</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> expires_at </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">timestamptz</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> not null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> created_at </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">timestamptz</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> default</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> now</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">()</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">);</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br></div></div></details><h3 id="_2-challenge-functions" tabindex="-1">2. Challenge Functions <a class="header-anchor" href="#_2-challenge-functions" aria-label="Permalink to "2. Challenge Functions""></a></h3><p>The WebAuthn flow requires generating random challenges that are verified later. Here's the registration challenge function:</p><details class="code-collapsible" open><summary>sql</summary><div class="language-sql vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">create or replace</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> function</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> passkey_challenge_registration</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(_body </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">returns</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> table</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> status</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> message</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> challenge </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> challenge_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_handle </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_name </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_display_name </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> exclude_credentials </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_context </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">security</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> definer</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">language</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> plpgsql</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">as</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $$</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">declare</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_name </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;"> =</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _body</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'userName'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_handle </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">begin</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> assert _user_name </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">is not null</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> and</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_name </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;"><></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> ''</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Generate new user handle (random 32 bytes)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_handle </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> gen_random_bytes(</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;">32</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Generate challenge (random 32 bytes)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> gen_random_bytes(</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;">32</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Store challenge for verification</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> insert into</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> passkey_challenges</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (challenge, user_id, operation, expires_at)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> values</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (_challenge, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'registration'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">now</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">()</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;"> +</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> interval </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'5 minutes'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> returning id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">into</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge_id;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> return</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> query </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">select</span></span>
<span class="line"><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> 200</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> encode(_challenge, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'base64'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">),</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge_id,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> encode(_user_handle, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'base64'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">),</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_name,</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> coalesce</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(_body</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'displayName'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, _user_name),</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> '[]'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> json_build_object(</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'userName'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, _user_name,</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'email'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, _body</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'email'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'deviceName'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, _body</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'deviceName'</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> );</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">end</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">$$;</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br><span class="line-number">52</span><br></div></div></details><p>The function returns exactly the columns NpgsqlRest expects:</p><ul><li><code>status</code>: HTTP status code (200 to proceed)</li><li><code>challenge</code>: Base64-encoded random bytes</li><li><code>challenge_id</code>: Reference for verification</li><li><code>user_handle</code>: Unique identifier stored with the passkey</li><li><code>user_context</code>: JSON that gets passed through to the completion function</li></ul><h3 id="_3-completion-functions" tabindex="-1">3. Completion Functions <a class="header-anchor" href="#_3-completion-functions" aria-label="Permalink to "3. Completion Functions""></a></h3><p>After the browser creates the credential, NpgsqlRest verifies the attestation and calls your completion function:</p><details class="code-collapsible" open><summary>sql</summary><div class="language-sql vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">create or replace</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> function</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> passkey_complete_registration</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _credential_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_handle </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _public_key </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _public_key_algorithm </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _sign_count </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _backup_eligible </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">boolean</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _transports </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">[],</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_context </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">returns</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> table</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">status</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">message</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, user_context </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">security</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> definer</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">language</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> plpgsql</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">as</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $$</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">declare</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_name </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;"> =</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_context</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'userName'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">begin</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Create the user</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> insert into</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> users (username, email)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> values</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (_user_name, _user_context</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'email'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> returning user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">into</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_id;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Store the passkey</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> insert into</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> passkeys (</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> credential_id, user_id, user_handle, public_key,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> public_key_algorithm, sign_count, transports,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> backup_eligible, device_name</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> )</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> values</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _credential_id, _user_id, _user_handle, _public_key,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _public_key_algorithm, _sign_count, _transports,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _backup_eligible, _user_context</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'deviceName'</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> );</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> return</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> query </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">select</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> 200</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> json_build_object(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'userId'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, _user_id);</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">end</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">$$;</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br></div></div></details><p>NpgsqlRest extracts the public key and algorithm from the CBOR attestation object before calling your function. You just store them.</p><h3 id="_4-authentication-function" tabindex="-1">4. Authentication Function <a class="header-anchor" href="#_4-authentication-function" aria-label="Permalink to "4. Authentication Function""></a></h3><p>For login, the completion function verifies the user and returns claims for cookie/JWT authentication:</p><details class="code-collapsible" open><summary>sql</summary><div class="language-sql vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">create or replace</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> function</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> passkey_complete_authenticate</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _credential_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _new_sign_count </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_context </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _analytics_data </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> default</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> null</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">returns</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> table</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> scheme </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> username </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> email </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> message</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> jsonb</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">security</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> definer</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">language</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> plpgsql</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">as</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $$</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">declare</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;"> =</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (_user_context</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'id'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">begin</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Update sign count and last used timestamp</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> update</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> passkeys</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> set</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> sign_count </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _new_sign_count, last_used_at </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> now</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">()</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> where</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> credential_id </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _credential_id;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Return user claims for authentication</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> return</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> query</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> select</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'cookies'</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> as</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> scheme,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> u.user_id,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> u.username,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> u.email,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> jsonb_build_object(</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'userId'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, u.user_id,</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'username'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, u.username,</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'email'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, u.email</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> )</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> from</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> users u</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> where</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> u.user_id </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_id;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">end</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">$$;</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br></div></div></details><p>The <code>scheme</code> column tells NpgsqlRest which authentication scheme to use (cookies, JWT bearer, etc.).</p><h3 id="_5-configuration" tabindex="-1">5. Configuration <a class="header-anchor" href="#_5-configuration" aria-label="Permalink to "5. Configuration""></a></h3><p>Enable passkey authentication in your <code>appsettings.json</code>. Minimal configuration:</p><details class="code-collapsible" open><summary>json</summary><div class="language-json vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">{</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">Auth</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> {</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">PasskeyAuth</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> {</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">Enabled</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> true</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">EnableRegister</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> true</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> }</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> }</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">}</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br></div></div></details><p>For custom SQL commands, specify the command settings:</p><details class="code-collapsible" open><summary>json</summary><div class="language-json vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">{</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">Auth</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> {</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">PasskeyAuth</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> {</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">Enabled</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> true</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">EnableRegister</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> true</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">ChallengeRegistrationCommand</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "select * from passkey_challenge_registration($1)"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">CompleteRegistrationCommand</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "select * from passkey_complete_registration($1,$2,$3,$4,$5,$6,$7,$8)"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">ChallengeAuthenticationCommand</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "select * from passkey_challenge_authentication($1,$2)"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">AuthenticateDataCommand</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "select * from passkey_authenticate_data($1)"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">CompleteAuthenticateCommand</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "select * from passkey_complete_authenticate($1,$2,$3,$4)"</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> }</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> }</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">}</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br></div></div></details><p>For the complete configuration reference, see <a href="/config/passkey-auth.html">Passkey Authentication Configuration</a>.</p><p>Key configuration options:</p><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Option</th><th>Description</th></tr></thead><tbody><tr><td><code>UserVerificationRequirement</code></td><td><code>"required"</code> = must use biometric/PIN; <code>"preferred"</code> = use if available</td></tr><tr><td><code>ResidentKeyRequirement</code></td><td><code>"required"</code> = true passwordless (no username field); <code>"preferred"</code> = user enters username first</td></tr><tr><td><code>AttestationConveyance</code></td><td><code>"none"</code> for most apps; <code>"direct"</code> to verify authenticator hardware</td></tr><tr><td><code>RateLimiterPolicy</code></td><td>Name of a configured rate limiter policy for brute-force protection</td></tr><tr><td><code>ConnectionName</code></td><td>Optional named connection for multi-database setups</td></tr><tr><td><code>CommandRetryStrategy</code></td><td>Retry strategy for transient database errors (default: <code>"default"</code>)</td></tr></tbody></table></div></div><h3 id="_6-client-side-implementation" tabindex="-1">6. Client-Side Implementation <a class="header-anchor" href="#_6-client-side-implementation" aria-label="Permalink to "6. Client-Side Implementation""></a></h3><p>The <a href="https://github.com/NpgsqlRest/npgsqlrest-docs/blob/main/examples/13_passkey/src/passkey.ts" target="_blank" rel="noreferrer">passkey.ts</a> script in the example provides a complete TypeScript implementation you can use as a template:</p><details class="code-collapsible" open><summary>typescript</summary><div class="language-typescript vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;">// Registration</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">const</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> result </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> await</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> register</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">{</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> userName</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'alice'</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> displayName</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'Alice'</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> deviceName</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'MacBook Pro'</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">}</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (result</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">.</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">success) </span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">{</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> console</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">.</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;">log</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'Registered with credential:'</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> result</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">.</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">credentialId)</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">;</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;">// Login</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">const</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> loginResult </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> await</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> login</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">{</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> userName</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'alice'</span><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> // Optional for discoverable credentials</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">}</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (loginResult</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">.</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">success) </span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">{</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> const</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> JSON</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">.</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;">parse</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(loginResult</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">.</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">response)</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> console</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">.</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;">log</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'Logged in as:'</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">.</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">username)</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">;</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">}</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br></div></div></details><p>The script handles:</p><ul><li>Base64URL encoding/decoding for WebAuthn data</li><li>RFC 7807 Problem Details error parsing</li><li>Browser capability detection</li><li>All three flows: registration, login, and adding passkeys to existing accounts</li></ul><h2 id="three-authentication-flows" tabindex="-1">Three Authentication Flows <a class="header-anchor" href="#three-authentication-flows" aria-label="Permalink to "Three Authentication Flows""></a></h2><p>NpgsqlRest supports three distinct passkey flows. Each endpoint internally executes a configured SQL command (typically a PostgreSQL function) that you define.</p><h3 id="_1-registration-new-user-with-passkey" tabindex="-1">1. Registration (New User with Passkey) <a class="header-anchor" href="#_1-registration-new-user-with-passkey" aria-label="Permalink to "1. Registration (New User with Passkey)""></a></h3><p>For new users signing up with a passkey. Creates both the user account and passkey.</p><div class="language-mermaid vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">mermaid</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">flowchart LR</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> subgraph Step1["Step 1: Get Challenge"]</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> direction TB</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> R1["POST /api/passkey/register/options</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ────────────────────────────</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Body:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> {</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> userName: string ✓</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> displayName?: string</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> email?: string</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> }"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> F1["passkey_challenge_registration($1)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ────────────────────────────</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $1 = JSON body</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Returns:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> • status, challenge, challenge_id</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> • user_handle, user_name</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> • exclude_credentials, user_context"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> RES1["Response:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> {</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> challenge, challengeId</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> rp: {id, name}</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user: {id, name, displayName}</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> pubKeyCredParams, timeout</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> excludeCredentials, userContext</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> }"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> R1 --> F1 --> RES1</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> end</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> subgraph Browser["Browser WebAuthn"]</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> WA["navigator.credentials.create()</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ────────────────────────────</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> User creates passkey</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (biometric/PIN)"]</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> end</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> subgraph Step2["Step 2: Complete Registration"]</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> direction TB</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> R2["POST /api/passkey/register</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ────────────────────────────</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Body:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> {</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> challengeId, credentialId ✓</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> attestationObject ✓</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> clientDataJSON ✓</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> transports?, userContext ✓</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> }"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> F2["① passkey_verify_challenge($1, $2)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $1=challengeId, $2='registration'</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Returns: challenge bytea</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ② passkey_complete_registration(...)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $1=credentialId, $2=userHandle</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $3=publicKey, $4=algorithm</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $5=transports, $6=backupEligible</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $7=userContext</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Returns: status, message"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> RES2["Response:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> {</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> success: true</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> credentialId: base64url</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> }"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> R2 --> F2 --> RES2</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> end</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Step1 --> Browser --> Step2</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br><span class="line-number">52</span><br><span class="line-number">53</span><br><span class="line-number">54</span><br><span class="line-number">55</span><br><span class="line-number">56</span><br><span class="line-number">57</span><br><span class="line-number">58</span><br><span class="line-number">59</span><br><span class="line-number">60</span><br><span class="line-number">61</span><br><span class="line-number">62</span><br><span class="line-number">63</span><br><span class="line-number">64</span><br><span class="line-number">65</span><br><span class="line-number">66</span><br><span class="line-number">67</span><br><span class="line-number">68</span><br><span class="line-number">69</span><br><span class="line-number">70</span><br><span class="line-number">71</span><br><span class="line-number">72</span><br><span class="line-number">73</span><br></div></div><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Endpoint</th><th>Executes SQL Command</th></tr></thead><tbody><tr><td><code>POST /api/passkey/register/options</code></td><td><code>ChallengeRegistrationCommand</code></td></tr><tr><td><code>POST /api/passkey/register</code></td><td><code>CompleteRegistrationCommand</code></td></tr></tbody></table></div></div><div class="warning custom-block"><p class="custom-block-title">Registration is Disabled by Default</p><p>The standalone registration flow (<code>EnableRegister: false</code> by default) allows anyone to create an account with just a passkey. In most production systems, you'll want additional verification before creating accounts—email confirmation, admin approval, invitation codes, or CAPTCHA validation.</p><p>For this reason, the recommended approach is:</p><ol><li>Create user accounts through your existing registration flow (with whatever verification you need)</li><li>Let users add passkeys to their verified accounts using the <strong>Add Passkey</strong> flow</li></ol><p>The standalone registration endpoints are available when you need them (set <code>EnableRegister: true</code>), and the <a href="https://github.com/NpgsqlRest/npgsqlrest-docs/tree/main/examples/13_passkey" target="_blank" rel="noreferrer">complete example</a> demonstrates both approaches.</p></div><h3 id="_2-add-passkey-existing-user" tabindex="-1">2. Add Passkey (Existing User) <a class="header-anchor" href="#_2-add-passkey-existing-user" aria-label="Permalink to "2. Add Passkey (Existing User)""></a></h3><p>For users who already have an account (maybe they logged in with password) and want to add a passkey. These endpoints require authentication.</p><div class="language-mermaid vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">mermaid</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">flowchart LR</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> subgraph Step1["Step 1: Get Challenge"]</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> direction TB</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> R1["POST /api/passkey/add/options</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> 🔐 Requires JWT</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ────────────────────────────</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Headers: Authorization: Bearer</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Body (optional):</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> {</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> deviceName?: string</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> }"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> F1["passkey_challenge_add_existing($1, $2)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ────────────────────────────</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $1 = JWT claims (JSON)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $2 = body JSON</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Returns:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> • status, challenge, challenge_id</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> • user_handle, user_name</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> • exclude_credentials, user_context"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> RES1["Response:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> {</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> challenge, challengeId</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user: {id, name, displayName}</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> excludeCredentials: [...existing]</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> userContext</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> }"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> R1 --> F1 --> RES1</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> end</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> subgraph Browser["Browser WebAuthn"]</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> WA["navigator.credentials.create()</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ────────────────────────────</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> User creates passkey</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (biometric/PIN)"]</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> end</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> subgraph Step2["Step 2: Complete Add"]</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> direction TB</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> R2["POST /api/passkey/add</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> 🔐 Requires JWT</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ────────────────────────────</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Body:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> {</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> challengeId, credentialId ✓</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> attestationObject ✓</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> clientDataJSON ✓</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> transports?, userContext ✓</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> }"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> F2["① passkey_verify_challenge($1, $2)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $1=challengeId, $2='registration'</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Returns: challenge bytea</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ② passkey_complete_add_existing(...)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $1=credentialId, $2=userHandle</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $3=publicKey, $4=algorithm</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $5=transports, $6=backupEligible</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $7=userContext</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Returns: status, message"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> RES2["Response:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> {</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> success: true</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> credentialId: base64url</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> }"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> R2 --> F2 --> RES2</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> end</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Step1 --> Browser --> Step2</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br><span class="line-number">52</span><br><span class="line-number">53</span><br><span class="line-number">54</span><br><span class="line-number">55</span><br><span class="line-number">56</span><br><span class="line-number">57</span><br><span class="line-number">58</span><br><span class="line-number">59</span><br><span class="line-number">60</span><br><span class="line-number">61</span><br><span class="line-number">62</span><br><span class="line-number">63</span><br><span class="line-number">64</span><br><span class="line-number">65</span><br><span class="line-number">66</span><br><span class="line-number">67</span><br><span class="line-number">68</span><br><span class="line-number">69</span><br><span class="line-number">70</span><br><span class="line-number">71</span><br><span class="line-number">72</span><br><span class="line-number">73</span><br><span class="line-number">74</span><br></div></div><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Endpoint (requires auth)</th><th>Executes SQL Command</th></tr></thead><tbody><tr><td><code>POST /api/passkey/add/options</code></td><td><code>ChallengeAddExistingUserCommand</code></td></tr><tr><td><code>POST /api/passkey/add</code></td><td><code>CompleteAddExistingUserCommand</code></td></tr></tbody></table></div></div><h3 id="_3-login" tabindex="-1">3. Login <a class="header-anchor" href="#_3-login" aria-label="Permalink to "3. Login""></a></h3><p>For authenticating with an existing passkey.</p><div class="language-mermaid vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">mermaid</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">flowchart LR</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> subgraph Step1["Step 1: Get Challenge"]</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> direction TB</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> R1["POST /api/passkey/login/options</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ────────────────────────────</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Body (optional):</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> {</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> userName?: string</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> }</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Empty = discoverable credentials"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> F1["passkey_challenge_authentication($1, $2)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ────────────────────────────</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $1 = userName (nullable)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $2 = body JSON</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Returns:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> • status, challenge, challenge_id</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> • allow_credentials"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> RES1["Response:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> {</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> challenge, challengeId</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> rpId, timeout</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> userVerification</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> allowCredentials?</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> }"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> R1 --> F1 --> RES1</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> end</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> subgraph Browser["Browser WebAuthn"]</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> WA["navigator.credentials.get()</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ────────────────────────────</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> User authenticates</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (biometric/PIN)"]</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> end</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> subgraph Step2["Step 2: Complete Login"]</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> direction TB</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> R2["POST /api/passkey/login</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ────────────────────────────</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Body:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> {</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> challengeId, credentialId ✓</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> authenticatorData ✓</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> clientDataJSON ✓</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> signature ✓</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> userHandle?</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> }"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> F2["① passkey_verify_challenge($1, $2)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $1=challengeId, $2='authentication'</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Returns: challenge bytea</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ② passkey_authenticate_data($1)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $1=credentialId</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Returns: public_key, sign_count</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_context</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ③ passkey_complete_authenticate(...)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $1=credentialId, $2=newSignCount</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $3=userContext</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Returns: scheme, user_id</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> username, email"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> RES2["Response:</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> {</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> accessToken: JWT</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> refreshToken?: JWT</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> expiresIn: number</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> }</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> OR Set-Cookie"]</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> R2 --> F2 --> RES2</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> end</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> Step1 --> Browser --> Step2</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br><span class="line-number">44</span><br><span class="line-number">45</span><br><span class="line-number">46</span><br><span class="line-number">47</span><br><span class="line-number">48</span><br><span class="line-number">49</span><br><span class="line-number">50</span><br><span class="line-number">51</span><br><span class="line-number">52</span><br><span class="line-number">53</span><br><span class="line-number">54</span><br><span class="line-number">55</span><br><span class="line-number">56</span><br><span class="line-number">57</span><br><span class="line-number">58</span><br><span class="line-number">59</span><br><span class="line-number">60</span><br><span class="line-number">61</span><br><span class="line-number">62</span><br><span class="line-number">63</span><br><span class="line-number">64</span><br><span class="line-number">65</span><br><span class="line-number">66</span><br><span class="line-number">67</span><br><span class="line-number">68</span><br><span class="line-number">69</span><br><span class="line-number">70</span><br><span class="line-number">71</span><br><span class="line-number">72</span><br><span class="line-number">73</span><br><span class="line-number">74</span><br><span class="line-number">75</span><br><span class="line-number">76</span><br><span class="line-number">77</span><br><span class="line-number">78</span><br></div></div><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Endpoint</th><th>Executes SQL Command</th></tr></thead><tbody><tr><td><code>POST /api/passkey/login/options</code></td><td><code>ChallengeAuthenticationCommand</code></td></tr><tr><td><code>POST /api/passkey/login</code></td><td><code>CompleteAuthenticateCommand</code></td></tr></tbody></table></div></div><h2 id="complete-configuration-reference" tabindex="-1">Complete Configuration Reference <a class="header-anchor" href="#complete-configuration-reference" aria-label="Permalink to "Complete Configuration Reference""></a></h2><p>This section provides a detailed reference for all PasskeyAuth configuration options and SQL commands.</p><h3 id="general-settings" tabindex="-1">General Settings <a class="header-anchor" href="#general-settings" aria-label="Permalink to "General Settings""></a></h3><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Setting</th><th>Default</th><th>Description</th></tr></thead><tbody><tr><td><code>Enabled</code></td><td><code>false</code></td><td>Master switch to enable passkey authentication</td></tr><tr><td><code>EnableRegister</code></td><td><code>false</code></td><td>Enable standalone registration (new users can sign up with passkey only)</td></tr><tr><td><code>RateLimiterPolicy</code></td><td><code>null</code></td><td>Name of a configured rate limiter policy to protect against brute-force</td></tr><tr><td><code>ConnectionName</code></td><td><code>null</code></td><td>Named connection for multi-database setups; uses default if null</td></tr><tr><td><code>CommandRetryStrategy</code></td><td><code>"default"</code></td><td>Retry strategy for transient database errors; set to null to disable</td></tr></tbody></table></div></div><h3 id="relying-party-settings" tabindex="-1">Relying Party Settings <a class="header-anchor" href="#relying-party-settings" aria-label="Permalink to "Relying Party Settings""></a></h3><p>The Relying Party (RP) identifies your application to the authenticator.</p><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Setting</th><th>Default</th><th>Description</th></tr></thead><tbody><tr><td><code>RelyingPartyId</code></td><td><code>null</code></td><td>Domain name (e.g., <code>"example.com"</code>). Auto-detected from request if null. <strong>Note:</strong> IP addresses are not permitted—use <code>"localhost"</code> for development</td></tr><tr><td><code>RelyingPartyName</code></td><td><code>null</code></td><td>Human-readable name shown during registration. Uses <code>ApplicationName</code> if null</td></tr><tr><td><code>RelyingPartyOrigins</code></td><td><code>[]</code></td><td>Allowed origins for validation (e.g., <code>["https://example.com"]</code>). Auto-detected if empty</td></tr></tbody></table></div></div><h3 id="endpoint-paths" tabindex="-1">Endpoint Paths <a class="header-anchor" href="#endpoint-paths" aria-label="Permalink to "Endpoint Paths""></a></h3><p>All paths are POST endpoints. Set to <code>null</code> to disable an endpoint.</p><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Setting</th><th>Default</th><th>Description</th></tr></thead><tbody><tr><td><code>AddPasskeyOptionsPath</code></td><td><code>"/api/passkey/add/options"</code></td><td>Get options for adding passkey to existing user (requires auth)</td></tr><tr><td><code>AddPasskeyPath</code></td><td><code>"/api/passkey/add"</code></td><td>Complete adding passkey to existing user (requires auth)</td></tr><tr><td><code>RegistrationOptionsPath</code></td><td><code>"/api/passkey/register/options"</code></td><td>Get options for new user registration (no auth required)</td></tr><tr><td><code>RegistrationPath</code></td><td><code>"/api/passkey/register"</code></td><td>Complete new user registration (no auth required)</td></tr><tr><td><code>LoginOptionsPath</code></td><td><code>"/api/passkey/login/options"</code></td><td>Get login challenge (no auth required)</td></tr><tr><td><code>LoginPath</code></td><td><code>"/api/passkey/login"</code></td><td>Complete authentication (no auth required)</td></tr></tbody></table></div></div><h3 id="webauthn-settings" tabindex="-1">WebAuthn Settings <a class="header-anchor" href="#webauthn-settings" aria-label="Permalink to "WebAuthn Settings""></a></h3><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Setting</th><th>Default</th><th>Description</th></tr></thead><tbody><tr><td><code>ChallengeTimeoutMinutes</code></td><td><code>5</code></td><td>How long challenges remain valid before expiring</td></tr><tr><td><code>ValidateSignCount</code></td><td><code>true</code></td><td>Validate signature counter to detect cloned authenticators</td></tr><tr><td><code>UserVerificationRequirement</code></td><td><code>"required"</code></td><td>See below</td></tr><tr><td><code>ResidentKeyRequirement</code></td><td><code>"required"</code></td><td>See below</td></tr><tr><td><code>AttestationConveyance</code></td><td><code>"none"</code></td><td>See below</td></tr></tbody></table></div></div><h4 id="userverificationrequirement" tabindex="-1">UserVerificationRequirement <a class="header-anchor" href="#userverificationrequirement" aria-label="Permalink to "UserVerificationRequirement""></a></h4><p>Controls whether biometric/PIN verification is required:</p><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Value</th><th>Behavior</th><th>Use Case</th></tr></thead><tbody><tr><td><code>"required"</code></td><td>User MUST verify with biometric or PIN</td><td>Banking, healthcare, any sensitive data</td></tr><tr><td><code>"preferred"</code></td><td>Request verification if available, proceed without if not</td><td>Most consumer apps</td></tr><tr><td><code>"discouraged"</code></td><td>Don't request verification (proves device possession only)</td><td>Low-security scenarios</td></tr></tbody></table></div></div><h4 id="residentkeyrequirement" tabindex="-1">ResidentKeyRequirement <a class="header-anchor" href="#residentkeyrequirement" aria-label="Permalink to "ResidentKeyRequirement""></a></h4><p>Controls discoverable credentials (true passwordless):</p><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Value</th><th>Behavior</th><th>Use Case</th></tr></thead><tbody><tr><td><code>"required"</code></td><td>Credential stored on authenticator; browser shows account picker</td><td>True passwordless (no username field)</td></tr><tr><td><code>"preferred"</code></td><td>Request discoverable if supported</td><td>Gradual migration to passwordless</td></tr><tr><td><code>"discouraged"</code></td><td>Server must provide credential ID</td><td>Username-first flows</td></tr></tbody></table></div></div><h4 id="attestationconveyance" tabindex="-1">AttestationConveyance <a class="header-anchor" href="#attestationconveyance" aria-label="Permalink to "AttestationConveyance""></a></h4><p>Controls whether to verify authenticator hardware:</p><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Value</th><th>Behavior</th><th>Use Case</th></tr></thead><tbody><tr><td><code>"none"</code></td><td>Accept any authenticator</td><td>Most apps (recommended)</td></tr><tr><td><code>"indirect"</code></td><td>Allow anonymized attestation</td><td>Rarely useful</td></tr><tr><td><code>"direct"</code></td><td>Request full attestation chain</td><td>Verify specific hardware models</td></tr><tr><td><code>"enterprise"</code></td><td>Enterprise-managed attestation</td><td>Corporate device policies</td></tr></tbody></table></div></div><h3 id="sql-commands-reference" tabindex="-1">SQL Commands Reference <a class="header-anchor" href="#sql-commands-reference" aria-label="Permalink to "SQL Commands Reference""></a></h3><p>NpgsqlRest calls your SQL functions at specific points in each flow. Here's when each command is executed and what it should return.</p><h4 id="challengeaddexistingusercommand" tabindex="-1">ChallengeAddExistingUserCommand <a class="header-anchor" href="#challengeaddexistingusercommand" aria-label="Permalink to "ChallengeAddExistingUserCommand""></a></h4><p><strong>When executed:</strong> User clicks "Add Passkey" in their account settings (they're already logged in)</p><p><strong>Endpoint:</strong> <code>POST /api/passkey/add/options</code></p><p><strong>Parameters:</strong></p><ul><li><code>$1</code> = <code>claims</code> (json): User claims from the authenticated session</li><li><code>$2</code> = <code>body</code> (json): Request body (e.g., <code>{ "deviceName": "My Phone" }</code>)</li></ul><p><strong>Expected return columns:</strong></p><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Column</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>status</code></td><td>int</td><td>HTTP status code. Return <code>200</code> to proceed, any other aborts</td></tr><tr><td><code>message</code></td><td>text</td><td>Error message when status ≠ 200</td></tr><tr><td><code>challenge</code></td><td>text</td><td>Base64-encoded random bytes (32 bytes recommended)</td></tr><tr><td><code>challenge_id</code></td><td>bigint/uuid/text</td><td>Server-side identifier to verify later</td></tr><tr><td><code>user_handle</code></td><td>text</td><td>Base64-encoded random bytes for WebAuthn user.id</td></tr><tr><td><code>user_name</code></td><td>text</td><td>Username shown in authenticator UI</td></tr><tr><td><code>user_display_name</code></td><td>text</td><td>Display name shown in authenticator UI</td></tr><tr><td><code>exclude_credentials</code></td><td>text</td><td>JSON array of existing credential IDs to prevent re-registration</td></tr><tr><td><code>user_context</code></td><td>json</td><td>Passed through to completion command (should contain user ID)</td></tr></tbody></table></div></div><p><strong>Example:</strong></p><details class="code-collapsible" open><summary>sql</summary><div class="language-sql vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">create or replace</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> function</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> passkey_challenge_add_existing</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _claims </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _body </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">returns</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> table</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> status</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">message</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, challenge </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, challenge_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_handle </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, user_name </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, user_display_name </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> exclude_credentials </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, user_context </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">language</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> plpgsql </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">as</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $$</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">declare</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;"> =</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (_claims</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'user_id'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;"> =</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> gen_random_bytes(</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;">32</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">);</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _existing_handle </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">begin</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Get existing user handle (or create new one)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> select</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_handle </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">into</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _existing_handle</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> from</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> passkeys </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">where</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_id </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">limit</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> 1</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _existing_handle </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">is</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> null</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> then</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _existing_handle </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> gen_random_bytes(</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;">32</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">);</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> end</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Store challenge</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> insert into</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> passkey_challenges (challenge, user_id, operation, expires_at)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> values</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (_challenge, _user_id, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'registration'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">now</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">()</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;"> +</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> interval </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'5 minutes'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> returning id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">into</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge_id;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> return</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> query</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> select</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> 200</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> encode(_challenge, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'base64'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">),</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge_id,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> encode(_existing_handle, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'base64'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">),</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _claims</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'username'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _claims</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'username'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">select</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> coalesce</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(jsonb_agg(jsonb_build_object(</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'type'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'public-key'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'id'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, encode(credential_id, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'base64'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> )), </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'[]'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::jsonb)::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> from</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> passkeys </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">where</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_id </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_id),</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> json_build_object(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'id'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, _user_id, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'deviceName'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, _body</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'deviceName'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">);</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">end</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">$$;</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br><span class="line-number">43</span><br></div></div></details><hr><h4 id="challengeregistrationcommand" tabindex="-1">ChallengeRegistrationCommand <a class="header-anchor" href="#challengeregistrationcommand" aria-label="Permalink to "ChallengeRegistrationCommand""></a></h4><p><strong>When executed:</strong> New user starts passkey-only registration (no existing account)</p><p><strong>Endpoint:</strong> <code>POST /api/passkey/register/options</code></p><p><strong>Parameters:</strong></p><ul><li><code>$1</code> = <code>body</code> (json): Request body with user info (e.g., <code>{ "userName": "alice", "email": "alice@example.com" }</code>)</li></ul><p><strong>Expected return columns:</strong> Same as <code>ChallengeAddExistingUserCommand</code></p><p><strong>Key difference:</strong> The <code>user_context</code> should NOT contain an <code>id</code> field—this tells the completion command to create a new user.</p><hr><h4 id="challengeauthenticationcommand" tabindex="-1">ChallengeAuthenticationCommand <a class="header-anchor" href="#challengeauthenticationcommand" aria-label="Permalink to "ChallengeAuthenticationCommand""></a></h4><p><strong>When executed:</strong> User initiates passkey login</p><p><strong>Endpoint:</strong> <code>POST /api/passkey/login/options</code></p><p><strong>Parameters:</strong></p><ul><li><code>$1</code> = <code>user_name</code> (text): Username if provided, NULL for discoverable credential flow</li><li><code>$2</code> = <code>body</code> (json): Request body</li></ul><p><strong>Expected return columns:</strong></p><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Column</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>status</code></td><td>int</td><td>HTTP status code (200 to proceed)</td></tr><tr><td><code>message</code></td><td>text</td><td>Error message when status ≠ 200</td></tr><tr><td><code>challenge</code></td><td>text</td><td>Base64-encoded random challenge</td></tr><tr><td><code>challenge_id</code></td><td>bigint/uuid/text</td><td>Server-side identifier</td></tr><tr><td><code>allow_credentials</code></td><td>text</td><td>JSON array of credential IDs for this user (empty for discoverable)</td></tr></tbody></table></div></div><p><strong>Example:</strong></p><details class="code-collapsible" open><summary>sql</summary><div class="language-sql vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">create or replace</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> function</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> passkey_challenge_authentication</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_name </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _body </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">returns</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> table</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> status</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">message</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, challenge </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> challenge_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, allow_credentials </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">language</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> plpgsql </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">as</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $$</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">declare</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;"> =</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> gen_random_bytes(</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;">32</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">);</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">begin</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- If username provided, look up user</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_name </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">is not null</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> and</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_name </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;"><></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> ''</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> then</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> select</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">into</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">from</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> users </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">where</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> username </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_name;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">is</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> null</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> then</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> return</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> query </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">select</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> 400</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'Bad request'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> return</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> end</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> end</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Store challenge</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> insert into</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> passkey_challenges (challenge, user_id, operation, expires_at)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> values</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (_challenge, _user_id, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'authentication'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">now</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">()</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;"> +</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> interval </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'5 minutes'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> returning id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">into</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge_id;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> return</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> query</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> select</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> 200</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> encode(_challenge, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'base64'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">),</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge_id,</span></span>
<span class="line"><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> coalesce</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">((</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> select</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> jsonb_agg(jsonb_build_object(</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'type'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'public-key'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'id'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, encode(credential_id, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'base64'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">),</span></span>
<span class="line"><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'transports'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, transports</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ))::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> from</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> passkeys </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">where</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> user_id </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_id</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> ), </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'[]'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">);</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">end</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">$$;</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br><span class="line-number">33</span><br><span class="line-number">34</span><br><span class="line-number">35</span><br><span class="line-number">36</span><br><span class="line-number">37</span><br><span class="line-number">38</span><br><span class="line-number">39</span><br><span class="line-number">40</span><br><span class="line-number">41</span><br><span class="line-number">42</span><br></div></div></details><hr><h4 id="verifychallengecommand" tabindex="-1">VerifyChallengeCommand <a class="header-anchor" href="#verifychallengecommand" aria-label="Permalink to "VerifyChallengeCommand""></a></h4><p><strong>When executed:</strong> After browser returns credential, before cryptographic verification</p><p><strong>Used by:</strong> ALL flows (add passkey, registration, and login)</p><p><strong>Parameters:</strong></p><ul><li><code>$1</code> = <code>challenge_id</code> (bigint/uuid/text): The challenge_id from the options response</li><li><code>$2</code> = <code>operation</code> (text): Either <code>"registration"</code> or <code>"authentication"</code></li></ul><p><strong>Expected return:</strong> Single column <code>challenge</code> (bytea) containing the original challenge bytes, or NULL if not found/expired</p><p><strong>Example:</strong></p><details class="code-collapsible" open><summary>sql</summary><div class="language-sql vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">create or replace</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> function</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> passkey_verify_challenge</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _operation </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">returns</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> bytea</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">language</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> plpgsql </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">as</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $$</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">declare</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">begin</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Delete and return the challenge (one-time use)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> delete</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> from</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> passkey_challenges</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> where</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> id </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge_id</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> and</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> operation </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _operation</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> and</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> expires_at </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">></span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> now</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">()</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> returning challenge </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">into</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> return</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _challenge;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">end</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">$$;</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br></div></div></details><hr><h4 id="authenticatedatacommand" tabindex="-1">AuthenticateDataCommand <a class="header-anchor" href="#authenticatedatacommand" aria-label="Permalink to "AuthenticateDataCommand""></a></h4><p><strong>When executed:</strong> During login, after challenge verification but before signature verification</p><p><strong>Endpoint:</strong> <code>POST /api/passkey/login</code></p><p><strong>Parameters:</strong></p><ul><li><code>$1</code> = <code>credential_id</code> (bytea): The credential ID from the browser</li></ul><p><strong>Expected return columns:</strong></p><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Column</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>status</code></td><td>int</td><td>HTTP status code (200 to proceed)</td></tr><tr><td><code>message</code></td><td>text</td><td>Error message when status ≠ 200</td></tr><tr><td><code>public_key</code></td><td>bytea</td><td>The stored public key for signature verification</td></tr><tr><td><code>public_key_algorithm</code></td><td>int</td><td>COSE algorithm ID (-7 for ES256, -257 for RS256)</td></tr><tr><td><code>sign_count</code></td><td>bigint</td><td>Current signature counter</td></tr><tr><td><code>user_context</code></td><td>json</td><td>Passed to CompleteAuthenticateCommand (typically contains user ID)</td></tr></tbody></table></div></div><p><strong>Example:</strong></p><details class="code-collapsible" open><summary>sql</summary><div class="language-sql vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">create or replace</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> function</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> passkey_authenticate_data</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(_credential_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">returns</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> table</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> status</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">message</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, public_key </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> public_key_algorithm </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, sign_count </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, user_context </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">language</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> plpgsql </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">as</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $$</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">begin</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> return</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> query</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> select</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> 200</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> p.public_key,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> p.public_key_algorithm,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> p.sign_count,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> json_build_object(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'id'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, p.user_id)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> from</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> passkeys p</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> where</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> p.credential_id </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _credential_id;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> if</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> not</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> found </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">then</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> return</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> query </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">select</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> 400</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'Bad request'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">null</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> end</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">end</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">$$;</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br></div></div></details><hr><h4 id="completeaddexistingusercommand" tabindex="-1">CompleteAddExistingUserCommand <a class="header-anchor" href="#completeaddexistingusercommand" aria-label="Permalink to "CompleteAddExistingUserCommand""></a></h4><p><strong>When executed:</strong> After successful attestation verification when adding passkey to existing user</p><p><strong>Endpoint:</strong> <code>POST /api/passkey/add</code></p><p><strong>Parameters:</strong></p><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>$1</code></td><td>bytea</td><td><code>credential_id</code> - Unique credential identifier</td></tr><tr><td><code>$2</code></td><td>bytea</td><td><code>user_handle</code> - WebAuthn user.id</td></tr><tr><td><code>$3</code></td><td>bytea</td><td><code>public_key</code> - Public key in COSE format</td></tr><tr><td><code>$4</code></td><td>int</td><td><code>algorithm</code> - COSE algorithm (-7 = ES256, -257 = RS256)</td></tr><tr><td><code>$5</code></td><td>text[]</td><td><code>transports</code> - Transport hints (e.g., <code>["internal", "hybrid"]</code>)</td></tr><tr><td><code>$6</code></td><td>boolean</td><td><code>backup_eligible</code> - Whether credential can be synced</td></tr><tr><td><code>$7</code></td><td>json</td><td><code>user_context</code> - From ChallengeAddExistingUserCommand</td></tr><tr><td><code>$8</code></td><td>json</td><td><code>analytics_data</code> - Optional client analytics with server-added IP</td></tr></tbody></table></div></div><p><strong>Expected return columns:</strong></p><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Column</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>status</code></td><td>int</td><td>HTTP status code (200 = success)</td></tr><tr><td><code>message</code></td><td>text</td><td>Error message when status ≠ 200</td></tr></tbody></table></div></div><hr><h4 id="completeregistrationcommand" tabindex="-1">CompleteRegistrationCommand <a class="header-anchor" href="#completeregistrationcommand" aria-label="Permalink to "CompleteRegistrationCommand""></a></h4><p><strong>When executed:</strong> After successful attestation verification for new user registration</p><p><strong>Endpoint:</strong> <code>POST /api/passkey/register</code></p><p><strong>Parameters:</strong> Same as <code>CompleteAddExistingUserCommand</code></p><p><strong>Expected return columns:</strong> Same as <code>CompleteAddExistingUserCommand</code></p><p><strong>Key difference:</strong> This command should CREATE a new user since <code>user_context</code> doesn't contain an existing user ID.</p><hr><h4 id="completeauthenticatecommand" tabindex="-1">CompleteAuthenticateCommand <a class="header-anchor" href="#completeauthenticatecommand" aria-label="Permalink to "CompleteAuthenticateCommand""></a></h4><p><strong>When executed:</strong> After successful signature verification during login</p><p><strong>Endpoint:</strong> <code>POST /api/passkey/login</code></p><p><strong>Parameters:</strong></p><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>$1</code></td><td>bytea</td><td><code>credential_id</code> - The credential that was used</td></tr><tr><td><code>$2</code></td><td>bigint</td><td><code>new_sign_count</code> - Updated signature counter</td></tr><tr><td><code>$3</code></td><td>json</td><td><code>user_context</code> - From AuthenticateDataCommand</td></tr><tr><td><code>$4</code></td><td>json</td><td><code>analytics_data</code> - Optional client analytics</td></tr></tbody></table></div></div><p><strong>Expected return columns:</strong></p><p>The return columns depend on your authentication scheme. For cookie authentication:</p><div class="table-container"><div class="table-wrapper"><table tabindex="0"><thead><tr><th>Column</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>scheme</code></td><td>text</td><td>Authentication scheme (e.g., <code>"cookies"</code>)</td></tr><tr><td>Any claim columns</td><td>various</td><td>Columns become claims (e.g., <code>user_id</code>, <code>username</code>, <code>email</code>)</td></tr><tr><td><code>message</code></td><td>jsonb</td><td>Optional JSON returned in response body</td></tr></tbody></table></div></div><p><strong>Example:</strong></p><details class="code-collapsible" open><summary>sql</summary><div class="language-sql vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">create or replace</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> function</span><span style="--shiki-light:#1E66F5;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic;"> passkey_complete_authenticate</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">(</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _credential_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bytea</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _new_sign_count </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">bigint</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_context </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _analytics_data </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">json</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> default</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> null</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">returns</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> table</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> scheme </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, username </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, email </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">text</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">message</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> jsonb</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">language</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> plpgsql </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">as</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> $$</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">declare</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_id </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;"> =</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (_user_context</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'id'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">)::</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">int</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">begin</span></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Update sign count</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> update</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> passkeys</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> set</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> sign_count </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _new_sign_count, last_used_at </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> now</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">()</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> where</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> credential_id </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _credential_id;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Optional: Log authentication</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _analytics_data </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">is not null</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> then</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> insert into</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> auth_audit_log (user_id, event_type, analytics_data, ip_address)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> values</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> (_user_id, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'passkey_login'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, _analytics_data, _analytics_data</span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">->></span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'ip'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">);</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> end</span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> if</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#9CA0B0;--shiki-light-font-style:italic;--shiki-dark:#6C7086;--shiki-dark-font-style:italic;"> -- Return claims for authentication</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> return</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> query</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> select</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> 'cookies'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, u.user_id, u.username, u.email,</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> jsonb_build_object(</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'userId'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, u.user_id, </span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;">'username'</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">, u.username)</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;"> from</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> users u </span><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">where</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> u.user_id </span><span style="--shiki-light:#179299;--shiki-dark:#94E2D5;">=</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;"> _user_id;</span></span>
<span class="line"><span style="--shiki-light:#8839EF;--shiki-dark:#CBA6F7;">end</span><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">;</span></span>
<span class="line"><span style="--shiki-light:#4C4F69;--shiki-dark:#CDD6F4;">$$;</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br></div></div></details><h3 id="column-name-configuration" tabindex="-1">Column Name Configuration <a class="header-anchor" href="#column-name-configuration" aria-label="Permalink to "Column Name Configuration""></a></h3><p>If your SQL functions use different column names, you can configure the mappings:</p><details class="code-collapsible" open><summary>json</summary><div class="language-json vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">{</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">PasskeyAuth</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> {</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">StatusColumnName</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "status"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">MessageColumnName</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "message"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">ChallengeColumnName</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "challenge"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">ChallengeIdColumnName</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "challenge_id"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">UserNameColumnName</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "user_name"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">UserDisplayNameColumnName</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "user_display_name"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">UserHandleColumnName</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "user_handle"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">ExcludeCredentialsColumnName</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "exclude_credentials"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">AllowCredentialsColumnName</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "allow_credentials"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">PublicKeyColumnName</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "public_key"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">PublicKeyAlgorithmColumnName</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "public_key_algorithm"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">SignCountColumnName</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "sign_count"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">UserContextColumnName</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "user_context"</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> }</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">}</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br></div></div></details><h3 id="analytics-data" tabindex="-1">Analytics Data <a class="header-anchor" href="#analytics-data" aria-label="Permalink to "Analytics Data""></a></h3><p>You can collect client-side analytics by passing <code>analyticsData</code> in completion requests. NpgsqlRest automatically adds the client's IP address:</p><details class="code-collapsible" open><summary>json</summary><div class="language-json vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">{</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">PasskeyAuth</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> {</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">ClientAnalyticsIpKey</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "ip"</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> }</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">}</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br></div></div></details><p>Set to <code>null</code> or empty string to disable IP collection.</p><h2 id="security-considerations" tabindex="-1">Security Considerations <a class="header-anchor" href="#security-considerations" aria-label="Permalink to "Security Considerations""></a></h2><h3 id="what-npgsqlrest-validates" tabindex="-1">What NpgsqlRest Validates <a class="header-anchor" href="#what-npgsqlrest-validates" aria-label="Permalink to "What NpgsqlRest Validates""></a></h3><p>NpgsqlRest performs all the cryptographic verification required by WebAuthn:</p><ul><li>Parses CBOR-encoded attestation objects</li><li>Extracts and validates public keys (EC P-256, EC P-384, RSA)</li><li>Verifies signatures using the stored public key</li><li>Validates origin and relying party ID</li><li>Checks challenge matches and hasn't expired</li><li>Optionally validates and updates signature counters</li></ul><h3 id="what-you-control" tabindex="-1">What You Control <a class="header-anchor" href="#what-you-control" aria-label="Permalink to "What You Control""></a></h3><p>Your SQL functions control the business logic:</p><ul><li>User creation and lookup</li><li>Credential storage and retrieval</li><li>Challenge expiration policy</li><li>Which users can register/authenticate</li><li>What claims are returned after authentication</li><li>Audit logging and analytics</li></ul><h3 id="rate-limiting" tabindex="-1">Rate Limiting <a class="header-anchor" href="#rate-limiting" aria-label="Permalink to "Rate Limiting""></a></h3><p>Always enable rate limiting on passkey endpoints:</p><details class="code-collapsible" open><summary>json</summary><div class="language-json vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">json</span><pre class="shiki shiki-themes catppuccin-latte catppuccin-mocha vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">{</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">PasskeyAuth</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> {</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">RateLimiterPolicy</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "passkey-limit"</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> },</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">RateLimiting</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> {</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">Policies</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> {</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">passkey-limit</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> {</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">Type</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#40A02B;--shiki-dark:#A6E3A1;"> "SlidingWindow"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">PermitLimit</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> 10</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">,</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> "</span><span style="--shiki-light:#1E66F5;--shiki-dark:#89B4FA;">WindowSeconds</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">"</span><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">:</span><span style="--shiki-light:#FE640B;--shiki-dark:#FAB387;"> 60</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> }</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> }</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;"> }</span></span>
<span class="line"><span style="--shiki-light:#7C7F93;--shiki-dark:#9399B2;">}</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br></div></div></details><h2 id="advantages-of-this-approach" tabindex="-1">Advantages of This Approach <a class="header-anchor" href="#advantages-of-this-approach" aria-label="Permalink to "Advantages of This Approach""></a></h2><h3 id="_1-sql-first-logic" tabindex="-1">1. SQL-First Logic <a class="header-anchor" href="#_1-sql-first-logic" aria-label="Permalink to "1. SQL-First Logic""></a></h3><p>Your authentication flow is defined in PostgreSQL functions. This means:</p><ul><li>Version control with your schema migrations</li><li>Testable with standard SQL testing tools</li><li>No application code changes needed to modify auth logic</li><li>Full power of SQL for complex business rules</li></ul><h3 id="_2-no-external-dependencies" tabindex="-1">2. No External Dependencies <a class="header-anchor" href="#_2-no-external-dependencies" aria-label="Permalink to "2. No External Dependencies""></a></h3><p>No FIDO2 libraries, no external authentication services. NpgsqlRest includes its own CBOR parser and cryptographic verification.</p><h3 id="_3-complete-control" tabindex="-1">3. Complete Control <a class="header-anchor" href="#_3-complete-control" aria-label="Permalink to "3. Complete Control""></a></h3><p>You decide:</p><ul><li>How users are created and stored</li><li>What metadata to track (device names, last used, etc.)</li><li>How to handle edge cases (duplicate registrations, account recovery)</li><li>What authentication scheme to use (cookies, JWT, custom)</li></ul><h3 id="_4-built-in-resilience" tabindex="-1">4. Built-in Resilience <a class="header-anchor" href="#_4-built-in-resilience" aria-label="Permalink to "4. Built-in Resilience""></a></h3><p>The passkey endpoints support:</p><ul><li>Connection retry for transient database failures</li><li>Command retry with configurable strategies</li><li>Named connections for multi-database architectures</li><li>Rate limiting to prevent abuse</li></ul><h3 id="_5-privacy-by-design" tabindex="-1">5. Privacy by Design <a class="header-anchor" href="#_5-privacy-by-design" aria-label="Permalink to "5. Privacy by Design""></a></h3><p>Since you only store public keys:</p><ul><li>No biometric data ever reaches your server</li><li>No password hashes to protect or rotate</li><li>Minimal personal data exposure in a breach</li><li>GDPR-friendly: public keys aren't personal data</li></ul><h2 id="getting-started" tabindex="-1">Getting Started <a class="header-anchor" href="#getting-started" aria-label="Permalink to "Getting Started""></a></h2><ol><li><strong>Create the schema</strong>: Set up your users, passkeys, and challenges tables</li><li><strong>Implement the SQL functions</strong>: Challenge creation, verification, completion</li><li><strong>Configure PasskeyAuth</strong>: Enable it and point to your functions</li><li><strong>Add client code</strong>: Use the provided <code>passkey.ts</code> as a starting point</li><li><strong>Enable rate limiting</strong>: Protect against brute-force attacks</li></ol><p>The <a href="https://github.com/NpgsqlRest/npgsqlrest-docs/tree/main/examples/13_passkey" target="_blank" rel="noreferrer">complete example</a> includes everything you need: schema, SQL functions, configuration, and client code. For all configuration options, see the <a href="/config/passkey-auth.html">Passkey Authentication Configuration</a> reference.</p><h2 id="conclusion" tabindex="-1">Conclusion <a class="header-anchor" href="#conclusion" aria-label="Permalink to "Conclusion""></a></h2><p>If you take one recommendation from this post: keep <code>EnableRegister</code> off, let users add passkeys to accounts created through your existing verified registration flow, and put a rate limiter on the endpoints. The rest—challenge storage, claims, audit logging—is ordinary SQL, version-controlled next to your schema.</p><div class="blog-nav" data-v-73e261c1><!----><!----><p data-v-73e261c1><strong data-v-73e261c1>More Blog Posts:</strong><br data-v-73e261c1><!--[--><!--[--><a href="/blog/the-backend-that-writes-itself-presentation" data-v-73e261c1>The Backend That Writes Itself (Slide Deck)</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/case-study-zero-backend-code" data-v-73e261c1>Case Study: 74 Endpoints, Zero Backend Code</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/typescript-codegen-walkthrough" data-v-73e261c1>TypeScript Code Generation Walkthrough</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/npgsqlrest-3.13-production-patterns" data-v-73e261c1>NpgsqlRest 3.13.0: Production Patterns</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/sql-rest-api" data-v-73e261c1>SQL REST API</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/excel-export-table-format-postgresql-npgsqlrest" data-v-73e261c1>Excel Exports Done Right</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/custom-types-multiset-rest-api" data-v-73e261c1>Custom Types & Multiset</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/performance-scalability-high-availability-npgsqlrest" data-v-73e261c1>Performance & High Availability</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/postgresql-rest-api-benchmark-2026" data-v-73e261c1>Benchmark 2026</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/end-to-end-static-type-checking-postgresql-typescript" data-v-73e261c1>End-to-End Type Checking</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/database-level-security-postgresql-authentication" data-v-73e261c1>Database-Level Security</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/multiple-auth-schemes-rbac-external-providers" data-v-73e261c1>Multiple Auth Schemes & RBAC</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/postgresql-bi-server-excel-csv-basic-auth" data-v-73e261c1>PostgreSQL BI Server</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/secure-image-uploads-postgresql-typescript" data-v-73e261c1>Secure Image Uploads</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/csv-excel-ingestion-postgresql-npgsqlrest" data-v-73e261c1>CSV & Excel Ingestion</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/real-time-chat-postgresql-sse-npgsqlrest" data-v-73e261c1>Real-Time Chat with SSE</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/external-api-calls-postgresql-http-types" data-v-73e261c1>External API Calls</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/reverse-proxy-postgresql-ai-service-npgsqlrest" data-v-73e261c1>Reverse Proxy & AI Service</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/npgsqlrest-vs-postgrest-supabase-comparison" data-v-73e261c1>NpgsqlRest vs PostgREST vs Supabase</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/blog/what-have-stored-procedures-ever-done-for-us" data-v-73e261c1>What Have Stored Procedures Done for Us?</a><!----><!--]--><!--]--></p><p data-v-73e261c1><strong data-v-73e261c1>Get Started:</strong><br data-v-73e261c1><!--[--><!--[--><a href="/config/passkey-auth" data-v-73e261c1>Passkey Configuration Reference</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="https://github.com/NpgsqlRest/npgsqlrest-docs/tree/main/examples/13_passkey" data-v-73e261c1>Complete Passkey Example</a><span data-v-73e261c1> · </span><!--]--><!--[--><a href="/guide/quick-start" data-v-73e261c1>Quick Start Guide</a><!----><!--]--><!--]--></p></div></div></div></main><footer class="VPDocFooter" data-v-39a288b8 data-v-e257564d><!--[--><!--]--><div class="edit-info" data-v-e257564d><div class="edit-link" data-v-e257564d><a class="VPLink link vp-external-link-icon no-icon edit-link-button" href="https://github.com/NpgsqlRest/npgsqlrest-docs/edit/main/docs/blog/passkey-sql-auth.md" target="_blank" rel="noreferrer" data-v-e257564d><!--[--><span class="vpi-square-pen edit-link-icon" data-v-e257564d></span> Edit this page on GitHub<!--]--></a></div><!----></div><nav class="prev-next" aria-labelledby="doc-footer-aria-label" data-v-e257564d><span class="visually-hidden" id="doc-footer-aria-label" data-v-e257564d>Pager</span><div class="pager" data-v-e257564d><a class="VPLink link pager-link prev" href="/blog/multiple-auth-schemes-rbac-external-providers.html" data-v-e257564d><!--[--><span class="desc" data-v-e257564d>Previous page</span><span class="title" data-v-e257564d>Multiple Auth Schemes & RBAC</span><!--]--></a></div><div class="pager" data-v-e257564d><a class="VPLink link pager-link next" href="/blog/end-to-end-static-type-checking-postgresql-typescript.html" data-v-e257564d><!--[--><span class="desc" data-v-e257564d>Next page</span><span class="title" data-v-e257564d>End-to-End Type Checking</span><!--]--></a></div></nav></footer><!--[--><!--[--><!--[--><div id="comments" class="giscus-wrapper" data-v-5337483f><h2 class="comments-heading" data-v-5337483f>Comments</h2><!----></div><div class="sponsor-footer" data-v-b93f2dbd><div class="sponsor-footer-content" data-v-b93f2dbd><span class="sponsor-heart" data-v-b93f2dbd>❤️</span><span class="sponsor-text" data-v-b93f2dbd>Support this project:</span><a href="https://patreon.com/vbconsulting" target="_blank" class="sponsor-link" data-v-b93f2dbd><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" data-v-b93f2dbd><path d="M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 7.463 3.097 9.746 2.602 2.124 6.19 2.078 9.402 1.836 1.883-.142 3.939-.358 5.477-1.528 1.564-1.19 1.873-3.298 1.972-5.151.134-2.51.187-5.016-.037-7.233zm-2.55 7.703c-.078 1.593-.383 3.473-1.795 4.334-1.351.824-3.151.973-4.686 1.083-2.891.207-6.063.276-8.514-1.399C3.106 17.39 2.924 14.34 2.95 11.67c.03-3.206.678-6.721 3.654-8.375 2.756-1.531 6.556-1.727 9.544-.872 2.339.669 4.351 2.678 4.355 5.217.007 2.583-.016 5.168-.095 7.273z" data-v-b93f2dbd></path><path d="M16.088 5.19c-1.008-.542-2.607-.48-3.441.388-.568.591-.773 1.389-.8 2.172-.034.993.068 2.013.361 2.953.307.985 1.034 1.848 2.077 2.012.878.138 1.83-.088 2.458-.732.7-.718.88-1.81.877-2.779-.003-.989-.084-1.99-.423-2.91-.268-.726-.654-1.328-1.109-1.104zm-.294 5.482c-.182.462-.581.771-1.091.696-.532-.078-.859-.533-1.04-1.005-.248-.647-.327-1.375-.312-2.06.01-.46.095-.939.371-1.313.345-.467.993-.447 1.426-.115.41.315.607.835.74 1.32.18.66.211 1.675-.094 2.477zM8.4 10.394c.233 1.084.755 2.299 1.907 2.579.934.227 1.993-.165 2.434-1.048.455-.91.346-1.995.166-2.972-.165-.893-.46-1.885-1.207-2.445-.745-.559-1.832-.457-2.479.192-.84.843-.958 2.138-.955 3.277.001.147.119.364.134.417zm1.363-2.808c.302-.321.805-.336 1.14-.054.445.375.618 1.015.75 1.558.131.54.212 1.156.082 1.703-.1.423-.378.821-.84.882-.469.062-.886-.306-1.088-.704-.326-.644-.401-1.426-.381-2.148.013-.465.104-.993.337-1.237z" data-v-b93f2dbd></path></svg> Patreon </a><span class="sponsor-separator" data-v-b93f2dbd>·</span><a href="https://buymeacoffee.com/vbilopavu" target="_blank" class="sponsor-link" data-v-b93f2dbd><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" data-v-b93f2dbd><path d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364z" data-v-b93f2dbd></path></svg> Buy Me a Coffee </a></div><div class="footer-license" data-v-b93f2dbd>Released under the MIT License.</div><div class="footer-copyright" data-v-b93f2dbd>Copyright © 2024-2026 VB Consulting</div></div><!--]--><!--]--><!--]--></div></div></div><!--[--><!--]--></div></div><!----><!--[--><!--]--></div></div>
<script>window.__VP_HASH_MAP__=JSON.parse("{\"about.md\":\"DQcoTdts\",\"annotations_allow-anonymous.md\":\"B0UuFGg2\",\"annotations_authorize.md\":\"CK7v-SQu\",\"annotations_basic-auth-command.md\":\"Csb6mha2\",\"annotations_basic-auth-realm.md\":\"DsMX0SDT\",\"annotations_basic-auth.md\":\"ClrT2h43\",\"annotations_body-parameter-name.md\":\"BfhRegXI\",\"annotations_buffer-rows.md\":\"BrfXbHRV\",\"annotations_cache-expires-in.md\":\"BRoExkyw\",\"annotations_cache-profile.md\":\"YD0xPH6k\",\"annotations_cached.md\":\"Z17IaMQ8\",\"annotations_column-names.md\":\"BgCUEwzC\",\"annotations_command-timeout.md\":\"DGZAbOuK\",\"annotations_connection.md\":\"kVRk6UjZ\",\"annotations_custom-parameters.md\":\"C_JoJInz\",\"annotations_define-param.md\":\"BEpTOvZh\",\"annotations_disabled.md\":\"DLiSD84Q\",\"annotations_enabled.md\":\"w_f0uh0_\",\"annotations_encrypt-decrypt.md\":\"BVL3DPHk\",\"annotations_error-code-policy.md\":\"CcDC1Uqp\",\"annotations_http-type.md\":\"Yj3KYiqW\",\"annotations_http.md\":\"DPCDKDD2\",\"annotations_index.md\":\"DSU3OO0a\",\"annotations_internal.md\":\"yWqX_Gop\",\"annotations_interval-format.md\":\"qinH4egR\",\"annotations_login.md\":\"CNtMBAwP\",\"annotations_logout.md\":\"Cle7WE1K\",\"annotations_mcp.md\":\"CUGE4v1n\",\"annotations_nested.md\":\"G3R18jm5\",\"annotations_new-line.md\":\"9HsK47GA\",\"annotations_openapi.md\":\"CYi2hImP\",\"annotations_param.md\":\"DCLc58O5\",\"annotations_parameter-hash.md\":\"DAN4SbBB\",\"annotations_parameter-substitution.md\":\"BAnY-Etv\",\"annotations_path.md\":\"4EcrWuEH\",\"annotations_proxy-out.md\":\"C6XnOMaD\",\"annotations_proxy.md\":\"C1XKqZ1u\",\"annotations_query-string-null-handling.md\":\"bDnQmkU1\",\"annotations_rate-limiter-policy.md\":\"C6rNxcxq\",\"annotations_raw.md\":\"mVR52W_-\",\"annotations_request-headers-mode.md\":\"CiBM2WBe\",\"annotations_request-headers-parameter-name.md\":\"B1w65j2m\",\"annotations_request-param-type.md\":\"D2ADbicS\",\"annotations_resolved-parameters.md\":\"CtIRul_U\",\"annotations_response-headers.md\":\"BjZbt4Z0\",\"annotations_response-null-handling.md\":\"BXujxmf9\",\"annotations_result-name.md\":\"oSFavaHs\",\"annotations_retry-strategy.md\":\"DJkjr3eT\",\"annotations_returns.md\":\"BXcLtPWm\",\"annotations_security-sensitive.md\":\"iGNlRYXu\",\"annotations_separator.md\":\"Bh3F0ZIY\",\"annotations_single.md\":\"6tuGsQhF\",\"annotations_skip.md\":\"Dh_dVMzK\",\"annotations_sse-events-level.md\":\"BzejI_6s\",\"annotations_sse-events-scope.md\":\"DSBEdrmZ\",\"annotations_sse.md\":\"VtYFy1W4\",\"annotations_table-format.md\":\"BKXSdBmi\",\"annotations_tags.md\":\"DmTy6McN\",\"annotations_test-claim.md\":\"B8Axvirh\",\"annotations_test-connection.md\":\"CBXd7WXg\",\"annotations_test-response.md\":\"BIQYW_7j\",\"annotations_test-setup.md\":\"CEjyi1f2\",\"annotations_test-tag.md\":\"CzXuyAz7\",\"annotations_test-teardown.md\":\"DrTOzo2w\",\"annotations_tsclient.md\":\"BNgVXSoY\",\"annotations_upload.md\":\"BJPkIDDG\",\"annotations_user-context.md\":\"D9DNdqIU\",\"annotations_user-parameters.md\":\"RuNgxxKs\",\"annotations_validate.md\":\"BYjsje_G\",\"annotations_void.md\":\"CdJ8Ec9f\",\"blog_case-study-zero-backend-code.md\":\"C_RsDiAE\",\"blog_csv-excel-ingestion-postgresql-npgsqlrest.md\":\"DSyRNYor\",\"blog_custom-types-multiset-rest-api.md\":\"DCC_Eq7R\",\"blog_database-level-security-postgresql-authentication.md\":\"DOXBN62P\",\"blog_draft-anniversary-vietnam-of-computer-science.md\":\"pc601xHv\",\"blog_draft-npgsqlrest-vs-sqlpage.md\":\"DbSL1Z8X\",\"blog_end-to-end-static-type-checking-postgresql-typescript.md\":\"CfE5da_h\",\"blog_excel-export-table-format-postgresql-npgsqlrest.md\":\"BuWwG6CQ\",\"blog_external-api-calls-postgresql-http-types.md\":\"C9zYiqiX\",\"blog_index.md\":\"De5ptA_4\",\"blog_mcp-server-postgresql-ai-tools-npgsqlrest.md\":\"DvISemRM\",\"blog_multiple-auth-schemes-rbac-external-providers.md\":\"BbvFjCJf\",\"blog_npgsqlrest-3.13-production-patterns.md\":\"jTlztVH-\",\"blog_npgsqlrest-3.19-sql-test-runner-watch-mode.md\":\"Dc7T3KVb\",\"blog_npgsqlrest-vs-postgrest-supabase-comparison.md\":\"00RFbAF5\",\"blog_passkey-sql-auth.md\":\"BRkr8TRo\",\"blog_performance-scalability-high-availability-npgsqlrest.md\":\"C-bwVXtr\",\"blog_postgresql-bi-server-excel-csv-basic-auth.md\":\"C9mEJphA\",\"blog_postgresql-rest-api-benchmark-2024.md\":\"DwEQmIoA\",\"blog_postgresql-rest-api-benchmark-2025.md\":\"Tbw6Blag\",\"blog_postgresql-rest-api-benchmark-2026.md\":\"BnlvOVbX\",\"blog_real-time-chat-postgresql-sse-npgsqlrest.md\":\"DHt1C23t\",\"blog_reverse-proxy-postgresql-ai-service-npgsqlrest.md\":\"BtaYnMBE\",\"blog_secure-image-uploads-postgresql-typescript.md\":\"Cp-wJyj4\",\"blog_sql-file-source-rest-api-from-plain-sql.md\":\"czCJfcgj\",\"blog_sql-rest-api.md\":\"C3OYX-j7\",\"blog_the-backend-that-writes-itself-presentation.md\":\"gFR1Oe0-\",\"blog_the-power-of-simplicity.md\":\"Cs_l8y-q\",\"blog_typescript-codegen-walkthrough.md\":\"DU5mkobw\",\"blog_web-scraping-postgresql-http-types-xml.md\":\"CCovU_63\",\"blog_what-have-stored-procedures-ever-done-for-us.md\":\"DbQNwRp9\",\"config_antiforgery.md\":\"7miFkqvJ\",\"config_auth.md\":\"BTlaq10R\",\"config_authentication-options.md\":\"CzhPcVla\",\"config_basic-auth-config.md\":\"DZCrOE7U\",\"config_cache-options.md\":\"Cq5Q0JXd\",\"config_claims-mapping.md\":\"DxUSHFxx\",\"config_codegen.md\":\"opHl7x9x\",\"config_command-retry.md\":\"BgAKKMgf\",\"config_config-section.md\":\"CwHYEVn1\",\"config_connection.md\":\"OYNJshxp\",\"config_cors.md\":\"Cusa--1Y\",\"config_data-protection.md\":\"Djdf76Tb\",\"config_error-handling.md\":\"DjvdMMV4\",\"config_external-auth.md\":\"bAqNP7C3\",\"config_forwarded-headers.md\":\"CGTpUREz\",\"config_health-checks.md\":\"BpWdFu-y\",\"config_http-client.md\":\"dRXgMQQQ\",\"config_http-files.md\":\"B6K6vnkA\",\"config_index.md\":\"CXfhm2PI\",\"config_latest.md\":\"BHuOeOrq\",\"config_logging.md\":\"B2g4l4DC\",\"config_mcp.md\":\"D08gYjRL\",\"config_npgsqlrest.md\":\"CnfMgb40\",\"config_openapi.md\":\"BCg7CKgl\",\"config_passkey-auth.md\":\"DHQzx-GX\",\"config_proxy.md\":\"DOTosU06\",\"config_rate-limiter.md\":\"wLNPwvhE\",\"config_response-compression.md\":\"CMmom6qe\",\"config_routine-options.md\":\"VEEBtjYd\",\"config_security-headers.md\":\"D2EP-6xq\",\"config_server.md\":\"BCPr-8rO\",\"config_sql-file-source.md\":\"5_GwGm0C\",\"config_static-files.md\":\"CxiUeSZr\",\"config_stats.md\":\"CXkhOJdL\",\"config_table-format.md\":\"CKOmZ6yI\",\"config_test-runner.md\":\"D9PY_12B\",\"config_thread-pool.md\":\"CQCaprkR\",\"config_top-level.md\":\"F9S_nf2B\",\"config_uploads.md\":\"DoJGjd4G\",\"config_validation.md\":\"lHNUsfic\",\"config_watch.md\":\"B_iL-Wg4\",\"examples_index.md\":\"D4o4oSds\",\"guide_annotations.md\":\"DVRiz6k_\",\"guide_authentication.md\":\"BaKZBV2r\",\"guide_changelog_index.md\":\"B7c1xVM5\",\"guide_changelog_v3.0.0.md\":\"BCZYSJDP\",\"guide_changelog_v3.0.1.md\":\"C5QoKstF\",\"guide_changelog_v3.1.0.md\":\"Xl_94wGw\",\"guide_changelog_v3.1.1.md\":\"CP1dTmO5\",\"guide_changelog_v3.1.2.md\":\"DaCWRd65\",\"guide_changelog_v3.1.3.md\":\"BMixLZ4Y\",\"guide_changelog_v3.10.0.md\":\"CCimYYDV\",\"guide_changelog_v3.11.0.md\":\"yn587hBX\",\"guide_changelog_v3.11.1.md\":\"DxsV24Au\",\"guide_changelog_v3.12.0.md\":\"BDVbkMod\",\"guide_changelog_v3.13.0.md\":\"kmKD3jrx\",\"guide_changelog_v3.14.0.md\":\"DqJvVHhw\",\"guide_changelog_v3.15.0.md\":\"DiRcMMNc\",\"guide_changelog_v3.15.1.md\":\"EipfUUTu\",\"guide_changelog_v3.15.2.md\":\"B_9nMiuK\",\"guide_changelog_v3.16.0.md\":\"z16WdNnJ\",\"guide_changelog_v3.16.1.md\":\"twyBHeJH\",\"guide_changelog_v3.16.2.md\":\"DJyi_HNy\",\"guide_changelog_v3.16.3.md\":\"BaS-l5LJ\",\"guide_changelog_v3.17.0.md\":\"DCqJkXNp\",\"guide_changelog_v3.18.0.md\":\"DhzJiwQ7\",\"guide_changelog_v3.18.1.md\":\"BUjRzlAL\",\"guide_changelog_v3.18.2.md\":\"D9Kg93qi\",\"guide_changelog_v3.19.0.md\":\"CnLwz7zb\",\"guide_changelog_v3.2.0.md\":\"DqMIq3GM\",\"guide_changelog_v3.2.1.md\":\"jsATl_CJ\",\"guide_changelog_v3.2.2.md\":\"DCWOjWem\",\"guide_changelog_v3.2.3.md\":\"BU0QClul\",\"guide_changelog_v3.2.4.md\":\"DlHy9dKX\",\"guide_changelog_v3.2.6.md\":\"Be7WrEBG\",\"guide_changelog_v3.2.7.md\":\"DnOXNKXQ\",\"guide_changelog_v3.3.0.md\":\"D_74S82V\",\"guide_changelog_v3.3.1.md\":\"DBVEhTOX\",\"guide_changelog_v3.4.0.md\":\"DWaLq7jU\",\"guide_changelog_v3.4.1.md\":\"BjkyM2ol\",\"guide_changelog_v3.4.2.md\":\"CwgXLNC8\",\"guide_changelog_v3.4.3.md\":\"CwIJ6zQw\",\"guide_changelog_v3.4.4.md\":\"Dun3dnAP\",\"guide_changelog_v3.4.5.md\":\"CCdH9iK4\",\"guide_changelog_v3.4.6.md\":\"DfISyzwO\",\"guide_changelog_v3.4.7.md\":\"PS46_sBY\",\"guide_changelog_v3.4.8.md\":\"C6Us0ANY\",\"guide_changelog_v3.5.0.md\":\"COe0eWbp\",\"guide_changelog_v3.6.0.md\":\"p7Q-OsI9\",\"guide_changelog_v3.6.1.md\":\"CCDqHGJF\",\"guide_changelog_v3.6.2.md\":\"Lg_0h_OI\",\"guide_changelog_v3.6.3.md\":\"CcLrwLcH\",\"guide_changelog_v3.7.0.md\":\"BsBWtBOJ\",\"guide_changelog_v3.8.0.md\":\"D7bLiXbw\",\"guide_changelog_v3.9.0.md\":\"Cm0S1Ft1\",\"guide_configuration.md\":\"CAstKOPi\",\"guide_faq.md\":\"DzLvzPIX\",\"guide_http-types.md\":\"6rK6DntP\",\"guide_index.md\":\"C-0777ru\",\"guide_installation.md\":\"KDi2i5hY\",\"guide_logging.md\":\"CT7Uqiy7\",\"guide_proxy.md\":\"DF7C5yHU\",\"guide_quick-start.md\":\"DtKtFUQX\",\"guide_sql-files.md\":\"yAmqICyN\",\"guide_sse.md\":\"CCeiNuHG\",\"guide_testing.md\":\"Mc_wQPBP\",\"index.md\":\"WBf3vDSd\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"NpgsqlRest - Automatic PostgreSQL Web Server\",\"description\":\"Automatic PostgreSQL Web Server - Create REST APIs for PostgreSQL databases in minutes.\",\"base\":\"/\",\"head\":[],\"router\":{\"prefetchLinks\":true},\"appearance\":true,\"themeConfig\":{\"logo\":\"/favicon.ico\",\"siteTitle\":\"NpgsqlRest\",\"nav\":[{\"text\":\"Guide\",\"items\":[{\"text\":\"Getting Started\",\"items\":[{\"text\":\"Overview\",\"link\":\"/guide/\"},{\"text\":\"Installation\",\"link\":\"/guide/installation\"},{\"text\":\"Quick Start\",\"link\":\"/guide/quick-start\"}]},{\"text\":\"Topics\",\"items\":[{\"text\":\"SQL File Endpoints\",\"link\":\"/guide/sql-files\"},{\"text\":\"Configuration Guide\",\"link\":\"/guide/configuration\"},{\"text\":\"Comment Annotations Guide\",\"link\":\"/guide/annotations\"},{\"text\":\"Authentication\",\"link\":\"/guide/authentication\"},{\"text\":\"Server-Sent Events\",\"link\":\"/guide/sse\"},{\"text\":\"HTTP Custom Types\",\"link\":\"/guide/http-types\"},{\"text\":\"Proxy Endpoints\",\"link\":\"/guide/proxy\"},{\"text\":\"Testing\",\"link\":\"/guide/testing\"},{\"text\":\"Logging\",\"link\":\"/guide/logging\"}]},{\"text\":\"Reference\",\"items\":[{\"text\":\"FAQ & Troubleshooting\",\"link\":\"/guide/faq\"},{\"text\":\"Changelog\",\"link\":\"/guide/changelog/\"}]}]},{\"text\":\"Examples\",\"link\":\"/examples/\"},{\"text\":\"Reference\",\"items\":[{\"text\":\"Annotations\",\"link\":\"/annotations/\"},{\"text\":\"Configuration\",\"link\":\"/config/\"}]},{\"text\":\"Blog\",\"link\":\"/blog/\"}],\"sidebar\":{\"/blog/\":[{\"text\":\"Tutorials\",\"collapsed\":false,\"items\":[{\"text\":\"Tests Are SQL Files Too\",\"link\":\"/blog/npgsqlrest-3.19-sql-test-runner-watch-mode\"},{\"text\":\"Case Study: 74 Endpoints, Zero Backend Code\",\"link\":\"/blog/case-study-zero-backend-code\"},{\"text\":\"TypeScript Code Generation Walkthrough\",\"link\":\"/blog/typescript-codegen-walkthrough\"},{\"text\":\"NpgsqlRest 3.13.0: Production Patterns\",\"link\":\"/blog/npgsqlrest-3.13-production-patterns\"},{\"text\":\"SQL REST API\",\"link\":\"/blog/sql-rest-api\"},{\"text\":\"SQL File Source\",\"link\":\"/blog/sql-file-source-rest-api-from-plain-sql\"},{\"text\":\"Database-Level Security\",\"link\":\"/blog/database-level-security-postgresql-authentication\"},{\"text\":\"Multiple Auth Schemes & RBAC\",\"link\":\"/blog/multiple-auth-schemes-rbac-external-providers\"},{\"text\":\"Passkey SQL Auth\",\"link\":\"/blog/passkey-sql-auth\"},{\"text\":\"End-to-End Type Checking\",\"link\":\"/blog/end-to-end-static-type-checking-postgresql-typescript\"},{\"text\":\"Real-Time Chat with SSE\",\"link\":\"/blog/real-time-chat-postgresql-sse-npgsqlrest\"},{\"text\":\"External API Calls\",\"link\":\"/blog/external-api-calls-postgresql-http-types\"},{\"text\":\"Web Scraping with HTTP Types\",\"link\":\"/blog/web-scraping-postgresql-http-types-xml\"},{\"text\":\"Reverse Proxy & AI Service\",\"link\":\"/blog/reverse-proxy-postgresql-ai-service-npgsqlrest\"},{\"text\":\"Secure Image Uploads\",\"link\":\"/blog/secure-image-uploads-postgresql-typescript\"},{\"text\":\"CSV & Excel Ingestion\",\"link\":\"/blog/csv-excel-ingestion-postgresql-npgsqlrest\"},{\"text\":\"PostgreSQL BI Server\",\"link\":\"/blog/postgresql-bi-server-excel-csv-basic-auth\"}]},{\"text\":\"Feature Deep Dives\",\"collapsed\":false,\"items\":[{\"text\":\"Excel Exports Done Right\",\"link\":\"/blog/excel-export-table-format-postgresql-npgsqlrest\"},{\"text\":\"Custom Types & Multiset\",\"link\":\"/blog/custom-types-multiset-rest-api\"},{\"text\":\"Performance & High Availability\",\"link\":\"/blog/performance-scalability-high-availability-npgsqlrest\"},{\"text\":\"What Have Stored Procedures Done for Us?\",\"link\":\"/blog/what-have-stored-procedures-ever-done-for-us\"},{\"text\":\"The Power of Simplicity\",\"link\":\"/blog/the-power-of-simplicity\"}]},{\"text\":\"Benchmarks & Comparisons\",\"collapsed\":false,\"items\":[{\"text\":\"Benchmark 2026\",\"link\":\"/blog/postgresql-rest-api-benchmark-2026\"},{\"text\":\"NpgsqlRest vs PostgREST vs Supabase\",\"link\":\"/blog/npgsqlrest-vs-postgrest-supabase-comparison\"}]},{\"text\":\"Other Links\",\"collapsed\":false,\"items\":[{\"text\":\"Guide\",\"link\":\"/guide/\"},{\"text\":\"Annotations Reference\",\"link\":\"/annotations/\"},{\"text\":\"Configuration Reference\",\"link\":\"/config/\"}]}],\"/guide/changelog/\":[{\"text\":\"Changelog\",\"items\":[{\"text\":\"Overview\",\"link\":\"/guide/changelog/\"},{\"text\":\"v3.19.0 (Latest)\",\"link\":\"/guide/changelog/v3.19.0\"},{\"text\":\"v3.18.2\",\"link\":\"/guide/changelog/v3.18.2\"},{\"text\":\"v3.18.1\",\"link\":\"/guide/changelog/v3.18.1\"},{\"text\":\"v3.18.0\",\"link\":\"/guide/changelog/v3.18.0\"},{\"text\":\"v3.17.0\",\"link\":\"/guide/changelog/v3.17.0\"},{\"text\":\"v3.16.3\",\"link\":\"/guide/changelog/v3.16.3\"},{\"text\":\"v3.16.2\",\"link\":\"/guide/changelog/v3.16.2\"},{\"text\":\"v3.16.1\",\"link\":\"/guide/changelog/v3.16.1\"},{\"text\":\"v3.16.0\",\"link\":\"/guide/changelog/v3.16.0\"},{\"text\":\"v3.15.2\",\"link\":\"/guide/changelog/v3.15.2\"},{\"text\":\"v3.15.1\",\"link\":\"/guide/changelog/v3.15.1\"},{\"text\":\"v3.15.0\",\"link\":\"/guide/changelog/v3.15.0\"},{\"text\":\"v3.14.0\",\"link\":\"/guide/changelog/v3.14.0\"},{\"text\":\"v3.13.0\",\"link\":\"/guide/changelog/v3.13.0\"},{\"text\":\"v3.12.0\",\"link\":\"/guide/changelog/v3.12.0\"},{\"text\":\"v3.11.1\",\"link\":\"/guide/changelog/v3.11.1\"},{\"text\":\"v3.11.0\",\"link\":\"/guide/changelog/v3.11.0\"},{\"text\":\"v3.10.0\",\"link\":\"/guide/changelog/v3.10.0\"},{\"text\":\"v3.9.0\",\"link\":\"/guide/changelog/v3.9.0\"},{\"text\":\"v3.8.0\",\"link\":\"/guide/changelog/v3.8.0\"},{\"text\":\"v3.7.0\",\"link\":\"/guide/changelog/v3.7.0\"},{\"text\":\"v3.6.3\",\"link\":\"/guide/changelog/v3.6.3\"},{\"text\":\"v3.6.2\",\"link\":\"/guide/changelog/v3.6.2\"},{\"text\":\"v3.6.1\",\"link\":\"/guide/changelog/v3.6.1\"},{\"text\":\"v3.6.0\",\"link\":\"/guide/changelog/v3.6.0\"},{\"text\":\"v3.5.0\",\"link\":\"/guide/changelog/v3.5.0\"},{\"text\":\"v3.4.8\",\"link\":\"/guide/changelog/v3.4.8\"},{\"text\":\"v3.4.7\",\"link\":\"/guide/changelog/v3.4.7\"},{\"text\":\"v3.4.6\",\"link\":\"/guide/changelog/v3.4.6\"},{\"text\":\"v3.4.5\",\"link\":\"/guide/changelog/v3.4.5\"},{\"text\":\"v3.4.4\",\"link\":\"/guide/changelog/v3.4.4\"},{\"text\":\"v3.4.3\",\"link\":\"/guide/changelog/v3.4.3\"},{\"text\":\"v3.4.2\",\"link\":\"/guide/changelog/v3.4.2\"},{\"text\":\"v3.4.1\",\"link\":\"/guide/changelog/v3.4.1\"},{\"text\":\"v3.4.0\",\"link\":\"/guide/changelog/v3.4.0\"},{\"text\":\"v3.3.1\",\"link\":\"/guide/changelog/v3.3.1\"},{\"text\":\"v3.3.0\",\"link\":\"/guide/changelog/v3.3.0\"},{\"text\":\"v3.2.7\",\"link\":\"/guide/changelog/v3.2.7\"},{\"text\":\"v3.2.6\",\"link\":\"/guide/changelog/v3.2.6\"},{\"text\":\"v3.2.4\",\"link\":\"/guide/changelog/v3.2.4\"},{\"text\":\"v3.2.3\",\"link\":\"/guide/changelog/v3.2.3\"},{\"text\":\"v3.2.2\",\"link\":\"/guide/changelog/v3.2.2\"},{\"text\":\"v3.2.1\",\"link\":\"/guide/changelog/v3.2.1\"},{\"text\":\"v3.2.0\",\"link\":\"/guide/changelog/v3.2.0\"},{\"text\":\"v3.1.3\",\"link\":\"/guide/changelog/v3.1.3\"},{\"text\":\"v3.1.2\",\"link\":\"/guide/changelog/v3.1.2\"},{\"text\":\"v3.1.1\",\"link\":\"/guide/changelog/v3.1.1\"},{\"text\":\"v3.1.0\",\"link\":\"/guide/changelog/v3.1.0\"},{\"text\":\"v3.0.1\",\"link\":\"/guide/changelog/v3.0.1\"},{\"text\":\"v3.0.0\",\"link\":\"/guide/changelog/v3.0.0\"}]},{\"text\":\"Other Links\",\"collapsed\":false,\"items\":[{\"text\":\"Guide\",\"link\":\"/guide/\"},{\"text\":\"Annotations Reference\",\"link\":\"/annotations/\"},{\"text\":\"Configuration Reference\",\"link\":\"/config/\"}]}],\"/examples/\":[{\"text\":\"Examples\",\"items\":[{\"text\":\"Overview\",\"link\":\"/examples/\"}]},{\"text\":\"Reference\",\"items\":[{\"text\":\"Guide\",\"link\":\"/guide/\"},{\"text\":\"Annotations Reference\",\"link\":\"/annotations/\"},{\"text\":\"Configuration Reference\",\"link\":\"/config/\"},{\"text\":\"Changelog\",\"link\":\"/guide/changelog/\"}]}],\"/guide/\":[{\"text\":\"Getting Started\",\"items\":[{\"text\":\"Overview\",\"link\":\"/guide/\"},{\"text\":\"Installation\",\"link\":\"/guide/installation\"},{\"text\":\"Quick Start\",\"link\":\"/guide/quick-start\"}]},{\"text\":\"Topics\",\"items\":[{\"text\":\"SQL File Endpoints\",\"link\":\"/guide/sql-files\"},{\"text\":\"Configuration Guide\",\"link\":\"/guide/configuration\"},{\"text\":\"Comment Annotations Guide\",\"link\":\"/guide/annotations\"},{\"text\":\"Authentication\",\"link\":\"/guide/authentication\"},{\"text\":\"Server-Sent Events\",\"link\":\"/guide/sse\"},{\"text\":\"HTTP Custom Types\",\"link\":\"/guide/http-types\"},{\"text\":\"Proxy Endpoints\",\"link\":\"/guide/proxy\"},{\"text\":\"Testing\",\"link\":\"/guide/testing\"},{\"text\":\"Logging\",\"link\":\"/guide/logging\"}]},{\"text\":\"Reference\",\"items\":[{\"text\":\"FAQ & Troubleshooting\",\"link\":\"/guide/faq\"},{\"text\":\"Changelog\",\"link\":\"/guide/changelog/\"},{\"text\":\"Examples\",\"link\":\"/examples/\"},{\"text\":\"Annotations Reference\",\"link\":\"/annotations/\"},{\"text\":\"Configuration Reference\",\"link\":\"/config/\"}]}],\"/annotations/\":[{\"text\":\"HTTP & Routing\",\"items\":[{\"text\":\"Overview\",\"link\":\"/annotations/\"},{\"text\":\"HTTP\",\"link\":\"/annotations/http\"},{\"text\":\"PATH\",\"link\":\"/annotations/path\"},{\"text\":\"PROXY\",\"link\":\"/annotations/proxy\"},{\"text\":\"PROXY_OUT\",\"link\":\"/annotations/proxy-out\"},{\"text\":\"INTERNAL\",\"link\":\"/annotations/internal\"},{\"text\":\"ENABLED\",\"link\":\"/annotations/enabled\"},{\"text\":\"DISABLED\",\"link\":\"/annotations/disabled\"},{\"text\":\"TAGS\",\"link\":\"/annotations/tags\"},{\"text\":\"OPENAPI\",\"link\":\"/annotations/openapi\"},{\"text\":\"MCP\",\"link\":\"/annotations/mcp\"},{\"text\":\"HTTP CUSTOM TYPES\",\"link\":\"/annotations/http-type\"}]},{\"text\":\"Authorization\",\"items\":[{\"text\":\"AUTHORIZE\",\"link\":\"/annotations/authorize\"},{\"text\":\"ALLOW_ANONYMOUS\",\"link\":\"/annotations/allow-anonymous\"},{\"text\":\"LOGIN\",\"link\":\"/annotations/login\"},{\"text\":\"LOGOUT\",\"link\":\"/annotations/logout\"},{\"text\":\"BASIC_AUTH\",\"link\":\"/annotations/basic-auth\"},{\"text\":\"BASIC_AUTH_REALM\",\"link\":\"/annotations/basic-auth-realm\"},{\"text\":\"BASIC_AUTH_COMMAND\",\"link\":\"/annotations/basic-auth-command\"}]},{\"text\":\"Request Configuration\",\"items\":[{\"text\":\"REQUEST_PARAM_TYPE\",\"link\":\"/annotations/request-param-type\"},{\"text\":\"REQUEST_HEADERS_MODE\",\"link\":\"/annotations/request-headers-mode\"},{\"text\":\"REQUEST_HEADERS_PARAMETER_NAME\",\"link\":\"/annotations/request-headers-parameter-name\"},{\"text\":\"BODY_PARAMETER_NAME\",\"link\":\"/annotations/body-parameter-name\"},{\"text\":\"QUERY_STRING_NULL_HANDLING\",\"link\":\"/annotations/query-string-null-handling\"},{\"text\":\"VALIDATE\",\"link\":\"/annotations/validate\"}]},{\"text\":\"Response Configuration\",\"items\":[{\"text\":\"Response Headers\",\"link\":\"/annotations/response-headers\"},{\"text\":\"RESPONSE_NULL_HANDLING\",\"link\":\"/annotations/response-null-handling\"},{\"text\":\"NESTED\",\"link\":\"/annotations/nested\"},{\"text\":\"SINGLE\",\"link\":\"/annotations/single\"},{\"text\":\"VOID\",\"link\":\"/annotations/void\"},{\"text\":\"RAW\",\"link\":\"/annotations/raw\"},{\"text\":\"SEPARATOR\",\"link\":\"/annotations/separator\"},{\"text\":\"NEW_LINE\",\"link\":\"/annotations/new-line\"},{\"text\":\"COLUMN_NAMES\",\"link\":\"/annotations/column-names\"}]},{\"text\":\"Caching & Performance\",\"items\":[{\"text\":\"CACHED\",\"link\":\"/annotations/cached\"},{\"text\":\"CACHE_EXPIRES_IN\",\"link\":\"/annotations/cache-expires-in\"},{\"text\":\"CACHE_PROFILE\",\"link\":\"/annotations/cache-profile\"},{\"text\":\"BUFFER_ROWS\",\"link\":\"/annotations/buffer-rows\"},{\"text\":\"COMMAND_TIMEOUT\",\"link\":\"/annotations/command-timeout\"},{\"text\":\"RETRY_STRATEGY\",\"link\":\"/annotations/retry-strategy\"},{\"text\":\"Interval Format\",\"link\":\"/annotations/interval-format\"}]},{\"text\":\"Server-Sent Events\",\"items\":[{\"text\":\"SSE\",\"link\":\"/annotations/sse\"},{\"text\":\"SSE_EVENTS_LEVEL\",\"link\":\"/annotations/sse-events-level\"},{\"text\":\"SSE_EVENTS_SCOPE\",\"link\":\"/annotations/sse-events-scope\"}]},{\"text\":\"Upload & Policies\",\"items\":[{\"text\":\"UPLOAD\",\"link\":\"/annotations/upload\"},{\"text\":\"ERROR_CODE_POLICY\",\"link\":\"/annotations/error-code-policy\"},{\"text\":\"RATE_LIMITER_POLICY\",\"link\":\"/annotations/rate-limiter-policy\"}]},{\"text\":\"Output & Code Generation\",\"items\":[{\"text\":\"TABLE_FORMAT\",\"link\":\"/annotations/table-format\"},{\"text\":\"TSCLIENT\",\"link\":\"/annotations/tsclient\"}]},{\"text\":\"Parameters\",\"items\":[{\"text\":\"PARAM\",\"link\":\"/annotations/param\"},{\"text\":\"PARAMETER_HASH\",\"link\":\"/annotations/parameter-hash\"},{\"text\":\"ENCRYPT / DECRYPT\",\"link\":\"/annotations/encrypt-decrypt\"},{\"text\":\"Parameter Value Substitution\",\"link\":\"/annotations/parameter-substitution\"},{\"text\":\"Resolved Parameters\",\"link\":\"/annotations/resolved-parameters\"}]},{\"text\":\"SQL File Annotations\",\"items\":[{\"text\":\"DEFINE_PARAM\",\"link\":\"/annotations/define-param\"},{\"text\":\"RESULT_NAME\",\"link\":\"/annotations/result-name\"},{\"text\":\"SKIP\",\"link\":\"/annotations/skip\"},{\"text\":\"RETURNS\",\"link\":\"/annotations/returns\"}]},{\"text\":\"Test Files (--test)\",\"items\":[{\"text\":\"TEST @setup\",\"link\":\"/annotations/test-setup\"},{\"text\":\"TEST @teardown\",\"link\":\"/annotations/test-teardown\"},{\"text\":\"TEST @connection\",\"link\":\"/annotations/test-connection\"},{\"text\":\"TEST @tag\",\"link\":\"/annotations/test-tag\"},{\"text\":\"TEST @claim\",\"link\":\"/annotations/test-claim\"},{\"text\":\"TEST @response\",\"link\":\"/annotations/test-response\"}]},{\"text\":\"Context & Security\",\"items\":[{\"text\":\"USER_CONTEXT\",\"link\":\"/annotations/user-context\"},{\"text\":\"USER_PARAMETERS\",\"link\":\"/annotations/user-parameters\"},{\"text\":\"CONNECTION\",\"link\":\"/annotations/connection\"},{\"text\":\"SECURITY_SENSITIVE\",\"link\":\"/annotations/security-sensitive\"},{\"text\":\"Custom Parameters\",\"link\":\"/annotations/custom-parameters\"}]},{\"text\":\"Other Links\",\"collapsed\":false,\"items\":[{\"text\":\"Guide\",\"link\":\"/guide/\"},{\"text\":\"Configuration Reference\",\"link\":\"/config/\"},{\"text\":\"Changelog\",\"link\":\"/guide/changelog/\"}]}],\"/config/\":[{\"text\":\"Core Settings\",\"items\":[{\"text\":\"Overview\",\"link\":\"/config/\"},{\"text\":\"Latest Default Configuration\",\"link\":\"/config/latest\"},{\"text\":\"Top-Level Settings\",\"link\":\"/config/top-level\"},{\"text\":\"Config Section\",\"link\":\"/config/config-section\"},{\"text\":\"NpgsqlRest Options\",\"link\":\"/config/npgsqlrest\"},{\"text\":\"Routine Options\",\"link\":\"/config/routine-options\"},{\"text\":\"Connection Settings\",\"link\":\"/config/connection\"},{\"text\":\"Server & SSL\",\"link\":\"/config/server\"}]},{\"text\":\"Security\",\"items\":[{\"text\":\"Authentication\",\"link\":\"/config/auth\"},{\"text\":\"External OAuth\",\"link\":\"/config/external-auth\"},{\"text\":\"Passkey Authentication\",\"link\":\"/config/passkey-auth\"},{\"text\":\"Authentication Options\",\"link\":\"/config/authentication-options\"},{\"text\":\"Claims Mapping\",\"link\":\"/config/claims-mapping\"},{\"text\":\"Basic Auth Config\",\"link\":\"/config/basic-auth-config\"},{\"text\":\"Validation\",\"link\":\"/config/validation\"},{\"text\":\"Antiforgery\",\"link\":\"/config/antiforgery\"},{\"text\":\"Data Protection\",\"link\":\"/config/data-protection\"},{\"text\":\"CORS\",\"link\":\"/config/cors\"},{\"text\":\"Security Headers\",\"link\":\"/config/security-headers\"},{\"text\":\"Forwarded Headers\",\"link\":\"/config/forwarded-headers\"}]},{\"text\":\"Features\",\"items\":[{\"text\":\"SQL File Source\",\"link\":\"/config/sql-file-source\"},{\"text\":\"Test Runner\",\"link\":\"/config/test-runner\"},{\"text\":\"Watch Mode\",\"link\":\"/config/watch\"},{\"text\":\"Proxy\",\"link\":\"/config/proxy\"},{\"text\":\"OpenAPI\",\"link\":\"/config/openapi\"},{\"text\":\"MCP\",\"link\":\"/config/mcp\"},{\"text\":\"HTTP Files\",\"link\":\"/config/http-files\"},{\"text\":\"Code Generation\",\"link\":\"/config/codegen\"},{\"text\":\"Upload Options\",\"link\":\"/config/uploads\"},{\"text\":\"Table Format\",\"link\":\"/config/table-format\"},{\"text\":\"HTTP Client\",\"link\":\"/config/http-client\"}]},{\"text\":\"Performance\",\"items\":[{\"text\":\"Response Compression\",\"link\":\"/config/response-compression\"},{\"text\":\"Cache Options\",\"link\":\"/config/cache-options\"},{\"text\":\"Rate Limiter\",\"link\":\"/config/rate-limiter\"},{\"text\":\"Command Retry\",\"link\":\"/config/command-retry\"},{\"text\":\"Thread Pool\",\"link\":\"/config/thread-pool\"}]},{\"text\":\"Infrastructure\",\"items\":[{\"text\":\"Logging\",\"link\":\"/config/logging\"},{\"text\":\"Static Files\",\"link\":\"/config/static-files\"},{\"text\":\"Error Handling\",\"link\":\"/config/error-handling\"},{\"text\":\"Health Checks\",\"link\":\"/config/health-checks\"},{\"text\":\"Stats\",\"link\":\"/config/stats\"}]},{\"text\":\"Other Links\",\"collapsed\":false,\"items\":[{\"text\":\"Guide\",\"link\":\"/guide/\"},{\"text\":\"Annotations Reference\",\"link\":\"/annotations/\"}]}]},\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/NpgsqlRest/NpgsqlRest\"},{\"icon\":{\"svg\":\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 24 24\\\" fill=\\\"currentColor\\\"><path d=\\\"M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 7.463 3.097 9.746 2.602 2.124 6.19 2.078 9.402 1.836 1.883-.142 3.939-.358 5.477-1.528 1.564-1.19 1.873-3.298 1.972-5.151.134-2.51.187-5.016-.037-7.233zm-2.55 7.703c-.078 1.593-.383 3.473-1.795 4.334-1.351.824-3.151.973-4.686 1.083-2.891.207-6.063.276-8.514-1.399C3.106 17.39 2.924 14.34 2.95 11.67c.03-3.206.678-6.721 3.654-8.375 2.756-1.531 6.556-1.727 9.544-.872 2.339.669 4.351 2.678 4.355 5.217.007 2.583-.016 5.168-.095 7.273z\\\"/><path d=\\\"M16.088 5.19c-1.008-.542-2.607-.48-3.441.388-.568.591-.773 1.389-.8 2.172-.034.993.068 2.013.361 2.953.307.985 1.034 1.848 2.077 2.012.878.138 1.83-.088 2.458-.732.7-.718.88-1.81.877-2.779-.003-.989-.084-1.99-.423-2.91-.268-.726-.654-1.328-1.109-1.104zm-.294 5.482c-.182.462-.581.771-1.091.696-.532-.078-.859-.533-1.04-1.005-.248-.647-.327-1.375-.312-2.06.01-.46.095-.939.371-1.313.345-.467.993-.447 1.426-.115.41.315.607.835.74 1.32.18.66.211 1.675-.094 2.477zM8.4 10.394c.233 1.084.755 2.299 1.907 2.579.934.227 1.993-.165 2.434-1.048.455-.91.346-1.995.166-2.972-.165-.893-.46-1.885-1.207-2.445-.745-.559-1.832-.457-2.479.192-.84.843-.958 2.138-.955 3.277.001.147.119.364.134.417zm1.363-2.808c.302-.321.805-.336 1.14-.054.445.375.618 1.015.75 1.558.131.54.212 1.156.082 1.703-.1.423-.378.821-.84.882-.469.062-.886-.306-1.088-.704-.326-.644-.401-1.426-.381-2.148.013-.465.104-.993.337-1.237z\\\"/></svg>\"},\"link\":\"https://patreon.com/vbconsulting\",\"ariaLabel\":\"Patreon\"},{\"icon\":{\"svg\":\"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 24 24\\\" fill=\\\"currentColor\\\"><path d=\\\"M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364z\\\"/></svg>\"},\"link\":\"https://buymeacoffee.com/vbilopavu\",\"ariaLabel\":\"Buy Me a Coffee\"}],\"editLink\":{\"pattern\":\"https://github.com/NpgsqlRest/npgsqlrest-docs/edit/main/docs/:path\",\"text\":\"Edit this page on GitHub\"},\"search\":{\"provider\":\"local\"}},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":false}");</script>
</body>
</html>