Wiki — Breadcrumb + Related Links
A docs-style wayfinding cluster for a deep reference page. A breadcrumb trail (Home › Guides › Security › Authentication › OAuth 2.0) links every intermediate level, marks the current page, collapses its middle into an expandable ellipsis when space runs out, and exposes a keyboard-accessible sibling switcher on the current crumb. Below sits a related-articles card grid and a previous/next pager with real titles, plus a scroll-spy table of contents — all self-contained vanilla JS.
MCP
Codice
:root {
--bg: #ffffff;
--bg-2: #f7f8fa;
--panel: #ffffff;
--ink: #1a1a1f;
--ink-2: #3a3a42;
--muted: #6b7280;
--line: rgba(20, 20, 30, 0.10);
--line-2: rgba(20, 20, 30, 0.18);
--link: #2563eb;
--link-hover: #1d4ed8;
--accent: #2563eb;
--note: #2563eb;
--tip: #16a34a;
--warn: #d97706;
--danger: #dc2626;
--code-bg: #f4f4f6;
--kbd-bg: #eceef2;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 14px;
--shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.06);
--shadow-md: 0 6px 24px rgba(20, 20, 30, 0.10);
--sans: "Inter", system-ui, -apple-system, sans-serif;
--serif: "Source Serif 4", Georgia, serif;
--mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", monospace;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: var(--sans);
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a { color: var(--link); text-decoration: none; }
a:hover { color: var(--link-hover); }
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: var(--r-sm);
}
.skip-link {
position: absolute;
left: 12px;
top: -48px;
background: var(--ink);
color: #fff;
padding: 8px 14px;
border-radius: var(--r-sm);
z-index: 60;
transition: top 0.15s ease;
}
.skip-link:focus { top: 12px; color: #fff; }
/* ---------- Topbar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 40;
background: rgba(255, 255, 255, 0.86);
backdrop-filter: saturate(180%) blur(10px);
border-bottom: 1px solid var(--line);
}
.topbar-inner {
max-width: 1280px;
margin: 0 auto;
display: flex;
align-items: center;
gap: 14px;
height: 56px;
padding: 0 22px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 9px;
font-weight: 800;
color: var(--ink);
letter-spacing: -0.01em;
}
.brand:hover { color: var(--ink); }
.brand-mark { color: var(--accent); font-size: 1.05rem; }
.brand-dim { color: var(--muted); font-weight: 700; }
.brand-ver {
font-family: var(--mono);
font-size: 0.72rem;
color: var(--muted);
border: 1px solid var(--line);
border-radius: 999px;
padding: 2px 8px;
}
.topbar-spacer { flex: 1; }
.topbar-nav { display: flex; gap: 4px; }
.topbar-nav a {
font-size: 0.86rem;
font-weight: 500;
color: var(--ink-2);
padding: 7px 11px;
border-radius: var(--r-sm);
}
.topbar-nav a:hover { background: var(--bg-2); color: var(--ink); }
.topbar-nav a[aria-current="true"] { color: var(--accent); background: rgba(37, 99, 235, 0.08); }
/* ---------- Shell layout ---------- */
.shell {
max-width: 1280px;
margin: 0 auto;
display: grid;
grid-template-columns: 232px minmax(0, 1fr) 196px;
gap: 36px;
padding: 30px 22px 80px;
align-items: start;
}
/* ---------- Sidebar ---------- */
.sidebar {
position: sticky;
top: 78px;
font-size: 0.875rem;
}
.side-heading {
margin: 0 0 10px;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--muted);
}
.side-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 1px; }
.side-list a {
display: block;
padding: 6px 11px;
border-radius: var(--r-sm);
color: var(--ink-2);
border-left: 2px solid transparent;
}
.side-list a:hover { background: var(--bg-2); color: var(--ink); }
.side-list a.is-current {
color: var(--accent);
font-weight: 600;
background: rgba(37, 99, 235, 0.07);
border-left-color: var(--accent);
}
/* ---------- Content column ---------- */
.content { min-width: 0; max-width: 760px; }
/* ---------- Breadcrumbs ---------- */
.crumbs {
margin-bottom: 22px;
padding-bottom: 14px;
border-bottom: 1px solid var(--line);
}
.crumb-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
font-size: 0.84rem;
}
.crumb { display: inline-flex; align-items: center; }
.crumb a {
color: var(--muted);
padding: 3px 5px;
border-radius: var(--r-sm);
font-weight: 500;
white-space: nowrap;
}
.crumb a:hover { color: var(--accent); background: var(--bg-2); }
.crumb-sep { color: var(--line-2); user-select: none; font-size: 0.95rem; }
/* Ellipsis collapse button */
.crumb-ellipsis { display: none; }
.crumb-sep-ellipsis { display: none; }
.crumb-expand {
font: inherit;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--muted);
background: var(--bg-2);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 1px 9px 4px;
cursor: pointer;
line-height: 1;
}
.crumb-expand:hover { color: var(--accent); border-color: var(--line-2); }
/* When collapsed (narrow), hide middle crumbs & show ellipsis */
.crumbs.is-collapsed .crumb-hide { display: none; }
.crumbs.is-collapsed .crumb-ellipsis,
.crumbs.is-collapsed .crumb-sep-ellipsis { display: inline-flex; }
/* After user expands, re-show everything even while collapsed */
.crumbs.is-collapsed.is-expanded .crumb-hide { display: inline-flex; }
.crumbs.is-collapsed.is-expanded .crumb-ellipsis,
.crumbs.is-collapsed.is-expanded .crumb-sep-ellipsis { display: none; }
/* Current crumb w/ sibling switcher */
.crumb-current { position: relative; }
.crumb-current-btn {
display: inline-flex;
align-items: center;
gap: 5px;
font: inherit;
font-weight: 600;
color: var(--ink);
background: transparent;
border: 1px solid transparent;
border-radius: var(--r-sm);
padding: 3px 7px;
cursor: pointer;
white-space: nowrap;
}
.crumb-current-btn:hover { background: var(--bg-2); border-color: var(--line); }
.crumb-current-btn[aria-expanded="true"] { background: var(--bg-2); border-color: var(--line-2); }
.crumb-caret { font-size: 0.7rem; color: var(--muted); transition: transform 0.15s ease; }
.crumb-current-btn[aria-expanded="true"] .crumb-caret { transform: rotate(180deg); }
.crumb-menu {
position: absolute;
top: calc(100% + 6px);
left: 0;
z-index: 30;
min-width: 200px;
list-style: none;
margin: 0;
padding: 5px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--shadow-md);
animation: menu-in 0.13s ease;
}
@keyframes menu-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: none; }
}
.crumb-menu a {
display: block;
padding: 7px 10px;
border-radius: var(--r-sm);
color: var(--ink-2);
font-size: 0.86rem;
font-weight: 500;
}
.crumb-menu a:hover,
.crumb-menu a:focus-visible { background: var(--bg-2); color: var(--ink); }
.crumb-menu a.is-active {
color: var(--accent);
font-weight: 600;
background: rgba(37, 99, 235, 0.07);
}
.crumb-menu a.is-active::after { content: "✓"; float: right; }
/* ---------- Article prose ---------- */
.article-head { margin-bottom: 26px; }
.eyebrow {
margin: 0 0 8px;
font-size: 0.74rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--accent);
}
.article h1 {
margin: 0 0 14px;
font-size: 2.15rem;
line-height: 1.15;
letter-spacing: -0.02em;
font-weight: 800;
}
.lede {
font-family: var(--serif);
font-size: 1.16rem;
line-height: 1.6;
color: var(--ink-2);
margin: 0;
}
.article p,
.article li {
font-family: var(--serif);
font-size: 1.04rem;
line-height: 1.65;
color: var(--ink-2);
}
.article p { margin: 0 0 18px; }
.article h2 {
font-family: var(--sans);
font-size: 1.4rem;
font-weight: 700;
letter-spacing: -0.01em;
margin: 38px 0 14px;
padding-bottom: 7px;
border-bottom: 1px solid var(--line);
scroll-margin-top: 80px;
}
.article strong { color: var(--ink); font-weight: 600; }
code {
font-family: var(--mono);
font-size: 0.86em;
background: var(--code-bg);
padding: 1.5px 6px;
border-radius: var(--r-sm);
color: var(--ink);
}
pre {
background: var(--code-bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 18px;
overflow-x: auto;
margin: 0 0 18px;
}
pre code {
background: none;
padding: 0;
font-size: 0.85rem;
line-height: 1.6;
color: var(--ink-2);
}
kbd {
font-family: var(--mono);
font-size: 0.78em;
background: var(--kbd-bg);
border: 1px solid var(--line-2);
border-bottom-width: 2px;
border-radius: var(--r-sm);
padding: 1px 6px;
color: var(--ink);
}
blockquote {
margin: 0 0 20px;
padding: 12px 16px;
background: rgba(37, 99, 235, 0.05);
border-left: 3px solid var(--note);
border-radius: 0 var(--r-sm) var(--r-sm) 0;
}
blockquote p { margin: 0; font-size: 0.98rem; }
.table-wrap { overflow-x: auto; margin: 0 0 20px; }
table {
width: 100%;
border-collapse: collapse;
font-family: var(--sans);
font-size: 0.88rem;
}
thead th {
text-align: left;
font-weight: 600;
color: var(--muted);
padding: 9px 12px;
border-bottom: 1.5px solid var(--line-2);
white-space: nowrap;
}
tbody td {
padding: 9px 12px;
border-bottom: 1px solid var(--line);
color: var(--ink-2);
vertical-align: top;
}
tbody tr:hover td { background: var(--bg-2); }
/* ---------- Prev / next pager ---------- */
.pager {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
margin-top: 40px;
}
.pager-link {
display: flex;
flex-direction: column;
gap: 4px;
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--panel);
transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.pager-link:hover {
border-color: var(--accent);
box-shadow: var(--shadow-sm);
transform: translateY(-1px);
}
.pager-next { text-align: right; align-items: flex-end; }
.pager-dir { font-size: 0.78rem; font-weight: 600; color: var(--accent); }
.pager-title { font-size: 0.98rem; font-weight: 600; color: var(--ink); }
/* ---------- Related articles ---------- */
.related {
margin-top: 46px;
padding-top: 26px;
border-top: 1px solid var(--line);
}
.related-title {
font-size: 1.25rem;
font-weight: 700;
letter-spacing: -0.01em;
margin: 0 0 16px;
}
.related-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.related-item a {
display: grid;
gap: 5px;
height: 100%;
padding: 15px 16px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--panel);
transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}
.related-item a:hover {
border-color: var(--accent);
box-shadow: var(--shadow-sm);
transform: translateY(-1px);
}
.related-kicker {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
}
.related-name { font-size: 0.98rem; font-weight: 600; color: var(--ink); line-height: 1.3; }
.related-desc { font-size: 0.85rem; color: var(--muted); line-height: 1.45; }
/* ---------- Right-rail TOC ---------- */
.toc {
position: sticky;
top: 78px;
font-size: 0.84rem;
}
.toc-title {
margin: 0 0 10px;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--muted);
}
.toc-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.toc-list a {
display: block;
padding: 4px 10px;
color: var(--muted);
border-left: 2px solid var(--line);
}
.toc-list a:hover { color: var(--accent); border-left-color: var(--accent); }
.toc-list a.is-active { color: var(--ink); font-weight: 600; border-left-color: var(--accent); }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 16px);
background: var(--ink);
color: #fff;
font-size: 0.86rem;
font-weight: 500;
padding: 10px 16px;
border-radius: 999px;
box-shadow: var(--shadow-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 70;
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 1040px) {
.shell { grid-template-columns: 220px minmax(0, 1fr); }
.toc { display: none; }
}
@media (max-width: 820px) {
.shell { grid-template-columns: 1fr; gap: 22px; padding: 22px 18px 70px; }
.sidebar {
position: static;
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px;
background: var(--bg-2);
}
.topbar-nav { display: none; }
.related-list { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.topbar-inner { padding: 0 14px; }
.article h1 { font-size: 1.7rem; }
.lede { font-size: 1.06rem; }
.crumb-list { font-size: 0.8rem; }
.pager { grid-template-columns: 1fr; }
.pager-next { text-align: left; align-items: flex-start; }
.crumb-menu { min-width: 180px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
/* ---------------- Toast helper ---------------- */
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2000);
}
/* ---------------- Breadcrumb collapse on narrow widths ---------------- */
var crumbs = document.getElementById("crumbs");
var COLLAPSE_AT = 640; // px
function syncCollapse() {
if (!crumbs) return;
var narrow = window.innerWidth <= COLLAPSE_AT;
crumbs.classList.toggle("is-collapsed", narrow);
if (!narrow) {
// back to wide: reset the expanded state so it re-collapses next time
crumbs.classList.remove("is-expanded");
var btn = crumbs.querySelector("[data-expand]");
if (btn) btn.setAttribute("aria-expanded", "false");
}
}
syncCollapse();
var resizeTimer = null;
window.addEventListener("resize", function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(syncCollapse, 120);
});
/* The "…" expander reveals the hidden middle crumbs */
var expandBtn = crumbs ? crumbs.querySelector("[data-expand]") : null;
if (expandBtn) {
expandBtn.addEventListener("click", function () {
crumbs.classList.add("is-expanded");
expandBtn.setAttribute("aria-expanded", "true");
// move focus to the first revealed crumb link for keyboard users
var revealed = crumbs.querySelector(".crumb-hide a");
if (revealed) revealed.focus();
toast("Showing full breadcrumb trail");
});
}
/* ---------------- Sibling-switch dropdown ---------------- */
var menuBtn = document.querySelector("[data-menu]");
var menuList = document.querySelector("[data-menu-list]");
if (menuBtn && menuList) {
var menuItems = Array.prototype.slice.call(
menuList.querySelectorAll('[role="menuitem"]')
);
function openMenu() {
menuList.hidden = false;
menuBtn.setAttribute("aria-expanded", "true");
document.addEventListener("click", onDocClick, true);
document.addEventListener("keydown", onMenuKey);
}
function closeMenu(focusBtn) {
menuList.hidden = true;
menuBtn.setAttribute("aria-expanded", "false");
document.removeEventListener("click", onDocClick, true);
document.removeEventListener("keydown", onMenuKey);
menuItems.forEach(function (i) { i.tabIndex = -1; });
if (focusBtn) menuBtn.focus();
}
function isOpen() {
return menuBtn.getAttribute("aria-expanded") === "true";
}
function focusItem(idx) {
if (idx < 0) idx = menuItems.length - 1;
if (idx >= menuItems.length) idx = 0;
menuItems.forEach(function (i) { i.tabIndex = -1; });
menuItems[idx].tabIndex = 0;
menuItems[idx].focus();
}
function onDocClick(e) {
if (!menuList.contains(e.target) && !menuBtn.contains(e.target)) {
closeMenu(false);
}
}
function onMenuKey(e) {
if (menuList.hidden) return;
var current = menuItems.indexOf(document.activeElement);
switch (e.key) {
case "ArrowDown":
e.preventDefault();
focusItem(current < 0 ? 0 : current + 1);
break;
case "ArrowUp":
e.preventDefault();
focusItem(current < 0 ? menuItems.length - 1 : current - 1);
break;
case "Home":
e.preventDefault();
focusItem(0);
break;
case "End":
e.preventDefault();
focusItem(menuItems.length - 1);
break;
case "Escape":
e.preventDefault();
closeMenu(true);
break;
case "Tab":
closeMenu(false);
break;
}
}
menuBtn.addEventListener("click", function (e) {
e.stopPropagation();
if (isOpen()) {
closeMenu(false);
} else {
openMenu();
focusItem(0);
}
});
menuBtn.addEventListener("keydown", function (e) {
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (!isOpen()) openMenu();
focusItem(0);
} else if (e.key === "ArrowUp") {
e.preventDefault();
if (!isOpen()) openMenu();
focusItem(menuItems.length - 1);
}
});
menuItems.forEach(function (item) {
item.addEventListener("click", function (e) {
e.preventDefault();
var label = item.textContent.trim();
// update active marker + the visible current-crumb label
menuItems.forEach(function (i) {
i.classList.remove("is-active");
i.removeAttribute("aria-current");
});
item.classList.add("is-active");
item.setAttribute("aria-current", "true");
var labelEl = menuBtn.querySelector(".crumb-current-label");
if (labelEl) labelEl.textContent = label;
closeMenu(true);
toast("Switched to “" + label + "”");
});
});
}
/* ---------------- Prev / next + related: intercept demo links ---------------- */
document.querySelectorAll(".pager-link, .related-item a").forEach(function (link) {
link.addEventListener("click", function (e) {
e.preventDefault();
var t =
link.querySelector(".pager-title, .related-name");
toast("Navigating to “" + (t ? t.textContent.trim() : "page") + "”");
});
});
/* ---------------- Scroll-spy for the right-rail TOC ---------------- */
var tocLinks = Array.prototype.slice.call(
document.querySelectorAll(".toc-list a")
);
var sections = tocLinks
.map(function (l) {
return document.getElementById(l.getAttribute("href").slice(1));
})
.filter(Boolean);
if (sections.length && "IntersectionObserver" in window) {
var spy = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
var id = entry.target.id;
tocLinks.forEach(function (l) {
l.classList.toggle(
"is-active",
l.getAttribute("href") === "#" + id
);
});
}
});
},
{ rootMargin: "-72px 0px -70% 0px", threshold: 0 }
);
sections.forEach(function (s) { spy.observe(s); });
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OAuth 2.0 — Aurora Docs</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Source+Serif+4:opsz,[email protected],400;8..60,500;8..60,600;8..60,700&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="topbar">
<div class="topbar-inner">
<a class="brand" href="#" aria-label="Aurora DB documentation home">
<span class="brand-mark" aria-hidden="true">◆</span>
<span class="brand-name">Aurora<span class="brand-dim">Docs</span></span>
</a>
<span class="brand-ver">v4.2</span>
<div class="topbar-spacer"></div>
<nav class="topbar-nav" aria-label="Sections">
<a href="#">Guides</a>
<a href="#" aria-current="true">Reference</a>
<a href="#">API</a>
<a href="#">Changelog</a>
</nav>
</div>
</header>
<div class="shell">
<aside class="sidebar" aria-label="Documentation sections">
<p class="side-heading">Authentication</p>
<ul class="side-list">
<li><a href="#">Overview</a></li>
<li><a href="#">API keys</a></li>
<li><a href="#">Sessions & cookies</a></li>
<li><a href="#" class="is-current" aria-current="page">OAuth 2.0</a></li>
<li><a href="#">SAML SSO</a></li>
<li><a href="#">Scoped tokens</a></li>
<li><a href="#">Rotating secrets</a></li>
</ul>
</aside>
<main id="main" class="content">
<!-- ============ BREADCRUMB TRAIL ============ -->
<nav class="crumbs" aria-label="Breadcrumb" id="crumbs">
<ol class="crumb-list">
<li class="crumb"><a href="#">Home</a></li>
<li class="crumb-sep" aria-hidden="true">›</li>
<li class="crumb crumb-hide"><a href="#">Guides</a></li>
<li class="crumb-sep crumb-hide" aria-hidden="true">›</li>
<li class="crumb crumb-hide"><a href="#">Security</a></li>
<li class="crumb-sep crumb-hide" aria-hidden="true">›</li>
<!-- Collapsed ellipsis: revealed crumbs hidden behind it on narrow widths -->
<li class="crumb crumb-ellipsis">
<button
type="button"
class="crumb-expand"
aria-label="Show hidden breadcrumb levels"
aria-expanded="false"
data-expand
>…</button>
</li>
<li class="crumb-sep crumb-sep-ellipsis" aria-hidden="true">›</li>
<li class="crumb"><a href="#">Authentication</a></li>
<li class="crumb-sep" aria-hidden="true">›</li>
<!-- Current crumb with a sibling switcher dropdown -->
<li class="crumb crumb-current">
<button
type="button"
class="crumb-current-btn"
aria-haspopup="true"
aria-expanded="false"
aria-current="page"
data-menu
>
<span class="crumb-current-label">OAuth 2.0</span>
<span class="crumb-caret" aria-hidden="true">▾</span>
</button>
<ul class="crumb-menu" role="menu" aria-label="Sibling pages" data-menu-list hidden>
<li role="none"><a role="menuitem" href="#" tabindex="-1">API keys</a></li>
<li role="none"><a role="menuitem" href="#" tabindex="-1">Sessions & cookies</a></li>
<li role="none"><a role="menuitem" href="#" tabindex="-1" class="is-active" aria-current="true">OAuth 2.0</a></li>
<li role="none"><a role="menuitem" href="#" tabindex="-1">SAML SSO</a></li>
<li role="none"><a role="menuitem" href="#" tabindex="-1">Scoped tokens</a></li>
</ul>
</li>
</ol>
</nav>
<!-- ============ ARTICLE ============ -->
<article class="article">
<header class="article-head">
<p class="eyebrow">Authentication · Reference</p>
<h1>OAuth 2.0 authorization</h1>
<p class="lede">
Authorize third-party applications against an Aurora DB workspace using the OAuth 2.0
authorization-code flow with PKCE. This page covers the grant lifecycle, scope model, and
token exchange endpoints exposed by the Verdant identity service.
</p>
</header>
<p>
Aurora DB delegates external authentication to <strong>Verdant</strong>, the platform
identity service shared across Project Nimbus deployments. An application registers a client,
requests a set of <a href="#scopes">scopes</a>, and redirects the user to the Verdant consent
screen. After consent, Verdant returns a short-lived authorization code that the client
exchanges for an access token and a refresh token.
</p>
<p>
We strongly recommend the authorization-code flow with PKCE for every client type, including
single-page and native apps. The implicit flow is deprecated and will be removed in
<code>v5.0</code>.
</p>
<h2 id="flow">Authorization-code flow</h2>
<p>
A typical exchange looks like the following. The <code>code_verifier</code> is generated by
the client and never leaves the device until the token exchange step.
</p>
<pre><code>POST /oauth/token HTTP/1.1
Host: auth.verdant.dev
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=ac_9f2c41be
&redirect_uri=https://app.example.com/callback
&client_id=cli_nimbus_42
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r</code></pre>
<h2 id="scopes">Scope model</h2>
<p>
Scopes are namespaced by resource. Request the narrowest set your integration needs — Verdant
surfaces every requested scope on the consent screen, and over-broad requests reduce install
conversion.
</p>
<div class="table-wrap">
<table>
<thead>
<tr><th>Scope</th><th>Grants</th><th>Tier</th></tr>
</thead>
<tbody>
<tr><td><code>db.read</code></td><td>Read rows from tables and views</td><td>Standard</td></tr>
<tr><td><code>db.write</code></td><td>Insert, update, and delete rows</td><td>Standard</td></tr>
<tr><td><code>schema.manage</code></td><td>Create or alter tables and indexes</td><td>Elevated</td></tr>
<tr><td><code>secrets.read</code></td><td>Read connection secrets and keys</td><td>Restricted</td></tr>
</tbody>
</table>
</div>
<blockquote>
<p>
<strong>Note —</strong> Restricted scopes require workspace-owner approval and are audited.
Press <kbd>R</kbd> in the dashboard to open the request log.
</p>
</blockquote>
<h2 id="refresh">Refreshing tokens</h2>
<p>
Access tokens live for one hour. Use the refresh token to mint a new pair without prompting
the user again. Refresh tokens rotate on every use — store only the latest value and discard
the previous one to keep your client within the replay window.
</p>
<!-- ============ PREV / NEXT PAGER ============ -->
<nav class="pager" aria-label="Page navigation">
<a class="pager-link pager-prev" href="#">
<span class="pager-dir">← Previous</span>
<span class="pager-title">Sessions & cookies</span>
</a>
<a class="pager-link pager-next" href="#">
<span class="pager-dir">Next →</span>
<span class="pager-title">SAML SSO</span>
</a>
</nav>
</article>
<!-- ============ RELATED ARTICLES ============ -->
<section class="related" aria-labelledby="related-h">
<h2 id="related-h" class="related-title">Related articles</h2>
<ul class="related-list">
<li class="related-item">
<a href="#">
<span class="related-kicker">Guide</span>
<span class="related-name">Rotating client secrets safely</span>
<span class="related-desc">A zero-downtime pattern for replacing OAuth client secrets across deployments.</span>
</a>
</li>
<li class="related-item">
<a href="#">
<span class="related-kicker">Reference</span>
<span class="related-name">Scoped tokens & least privilege</span>
<span class="related-desc">How scope intersections are resolved when a token carries overlapping grants.</span>
</a>
</li>
<li class="related-item">
<a href="#">
<span class="related-kicker">Tutorial</span>
<span class="related-name">Wiring SAML SSO to the Verdant Empire</span>
<span class="related-desc">Map enterprise IdP assertions onto Aurora DB workspace roles step by step.</span>
</a>
</li>
<li class="related-item">
<a href="#">
<span class="related-kicker">Concept</span>
<span class="related-name">PKCE, explained from scratch</span>
<span class="related-desc">Why the proof-key exchange closes the interception gap in public clients.</span>
</a>
</li>
</ul>
</section>
</main>
<aside class="toc" aria-label="On this page">
<p class="toc-title">On this page</p>
<ul class="toc-list">
<li><a href="#flow">Authorization-code flow</a></li>
<li><a href="#scopes">Scope model</a></li>
<li><a href="#refresh">Refreshing tokens</a></li>
</ul>
</aside>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Breadcrumb + Related Links
A wayfinding cluster for a deep documentation page, modeled on developer reference sites. The breadcrumb trail walks Home › Guides › Security › Authentication › OAuth 2.0, where every intermediate level is a link and the final crumb is the current page. When the viewport gets too narrow the middle of the trail collapses into a single … button; pressing it expands the hidden levels in place and moves focus to the first revealed link. The current crumb doubles as a sibling switcher — open it to jump to neighbouring pages like API keys, SAML SSO, or Scoped tokens.
That switcher is a real ARIA menu: open it with a click, Enter, Space, or the arrow keys, walk the items with ↑ and ↓, jump with Home and End, and close with Esc or an outside click. Choosing a sibling updates the live crumb label and confirms with a toast. Beneath the article, a related-articles card grid and a previous/next pager carry real titles, while a sticky right-rail table of contents highlights the section you are reading.
Everything is plain HTML, CSS, and vanilla JS with no dependencies. Inter drives the chrome, a serif handles long-form prose, and JetBrains Mono renders code and keyboard hints. The layout reflows down to about 360px — the sidebar becomes an inline panel, the pager and related cards stack, and hover, active, and focus states stay visible throughout.
Illustrative UI only — fictional articles, products, and data.