Files
Miranda LimonczenkoandClaude Sonnet 5 e7d9c88cbc fix(docs): resolve remaining heading-order issues found in Pass 2 diagnostic (#48664)
## Problem

After merging [#48456](https://github.com/supabase/supabase/pull/48456)
(shared components) and
[#48459](https://github.com/supabase/supabase/pull/48459) (per-page
content fixes), a follow-up diagnostic pass found 22 remaining
heading-order violations, logged as Pass 2 in the [triage
report](https://app.notion.com/p/supabase/Playwright-E2E-Triage-Reports-3ab5004b775f81e3bc60d058fa5a02c1).
None of them were caught by the earlier fixes because they came from
places that scan didn't check: shared partials, raw HTML heading tags
written directly in MDX, and a couple of shared/interactive components
rendering hardcoded heading levels.

## Solution

- `_partials/social_provider_setup.mdx`: `#### Local development` →
`###`, matching the `##` that always precedes it on all 14 social-login
pages.
- `guides/database/functions.mdx` and
`guides/integrations/vercel-marketplace.mdx`: replaced raw `<h4>`/`<h5>`
tags with correctly-nested real headings (`### Planets`/`### People`;
`#### Deploy a Next.js app...`) — no styling workarounds needed since
they nest naturally one level below their parent section.
- `auth/quickstarts/{nextjs,react-native,react,astrojs}.mdx`: these 4
pages had no heading at all before the embedded
`_partials/api_settings.mdx` partial's own `### Get API details`
heading, so added a `## Quickstart` heading above the walkthrough to
give it a valid parent.
- `packages/ui`'s `Accordion` component: Radix's
`AccordionPrimitive.Header` renders as an unconditional `<h3>`
regardless of where the accordion is used. That's shared across Studio,
www, and design-system, not just docs, and surfaced on docs'
vendor-agnostic telemetry page. Now rendered via `asChild` onto a plain
`div` instead, since a generic accordion has no way to know what heading
level (if any) is valid in a given page.
- SQL-to-REST translator tool (`/docs/guides/api/sql-to-rest`): its
`Assumptions`/`FAQs` section labels were hardcoded `<h3>` with no `h2`
anywhere on the page. Converted to styled spans rather than promoting to
a real `<h2>`, because real h1/h2/h3 tags in this codebase force a prose
font-size that utility classes can't override — promoting the tag would
have visibly changed its size.
- `RealtimeLimitsEstimator` (embedded on both `postgres-changes` and
`benchmarks`): its 3 section headings were hardcoded `<h4>`, but the two
embedding pages need different levels (h3 vs h4) for that spot to be
valid — no single correct heading level. Converted to styled spans, same
pattern used throughout this project for components embedded at varying
heading depths.

## Manual testing

1. Check out this branch and run `pnpm dev:docs`.
2. Visit `/docs/guides/auth/social-login/auth-github` (or any other
provider page) and confirm the "Local development" callout under "Find
your callback URL" still looks and reads the same.
3. Visit `/docs/guides/database/functions` → "Returning data sets" tab
and confirm the "Planets" / "People" table captions still look the same.
4. Visit `/docs/guides/integrations/vercel-marketplace` → "Quickstart" →
"Via template" and confirm the CTA card title still looks the same.
5. Visit `/docs/guides/auth/quickstarts/nextjs` (or
react-native/react/astrojs) and confirm a "Quickstart" heading now
appears above the walkthrough, and "Get API details" still renders
correctly further down.
6. Run `pnpm dev:design-system` and open
`/design-system/docs/components/accordion` — expand/collapse an item and
confirm it still animates and looks identical; inspect the DOM and
confirm the trigger's wrapper is a `div`, not an `h3`.
7. Visit `/docs/guides/api/sql-to-rest`, translate any query, and
confirm the "Assumptions"/"FAQs" section labels still look the same.
8. Visit `/docs/guides/realtime/postgres-changes` and
`/docs/guides/realtime/benchmarks`, scroll to the connection-limits
calculator, and confirm its section labels still look the same on both
pages.
9. (Optional, for a full re-check) Run `pnpm e2e:docs:a11y --all`
against a deployed preview of this branch — only `/docs/guides/cli`
(pre-existing 404, unrelated to headings) should fail; every other page
should pass.

Verified with a full Playwright run against a real preview deployment:
**756 passed, 1 failed** (`/docs/guides/cli`, the pre-existing unrelated
404). Zero heading-order violations remain.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Documentation**
- Added clearly labeled Quickstart sections to Astro, Next.js, React
Native, and React authentication guides.
- Improved heading hierarchy and formatting across social provider
setup, database functions, and deployment documentation.
- Updated estimator and SQL-to-REST section presentation for more
consistent content structure.

- **Bug Fixes**
- Improved accordion trigger layout while preserving existing behavior,
styling, accessibility, and icon display.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 09:00:29 -07:00

665 lines
18 KiB
Plaintext

---
title: 'Use Supabase Auth with Astro'
subtitle: 'Learn how to configure Supabase Auth for Astro with server-side rendering.'
breadcrumb: 'Auth Quickstarts'
hideToc: true
---
## Quickstart
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Create a new Supabase project">
Head over to [database.new](https://database.new) and create a new Supabase project.
Your new database has a table for storing your users. You can see that this table is currently empty by running some SQL in the [SQL Editor](/dashboard/project/_/sql/new).
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```sql name=SQL_EDITOR
select * from auth.users;
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Create an Astro app">
Create a new Astro app using the `npm create` command.
<$Partial path="uiLibCta.mdx" />
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash name=Terminal
npm create astro@latest my-app
cd my-app
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Install Supabase libraries and Node adapter">
Install the `@supabase/supabase-js` client library, `@supabase/ssr` for server-side auth, and the `@astrojs/node` adapter to enable server-side rendering.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash name=Terminal
npm install @supabase/supabase-js @supabase/ssr @astrojs/node
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Configure Astro for SSR">
Update your `astro.config.mjs` to enable server-side rendering with the Node adapter.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```js name=astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
export default defineConfig({
output: "server",
adapter: node({
mode: "standalone",
}),
});
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Declare Supabase Environment Variables">
Create a `.env.local` file and populate with your Supabase connection variables:
<ProjectConfigVariables variable="url" />
<ProjectConfigVariables variable="publishable" />
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```text name=.env.local
PUBLIC_SUPABASE_URL=your-project-url
PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_key
```
<$Partial path="api_settings.mdx" variables={{ "framework": "astro", "tab": "frameworks" }} />
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={6}>
<StepHikeCompact.Details title="Create a Supabase client helper">
Create a utility file to initialize the Supabase client with SSR support:
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```ts name=src/lib/supabase.ts
import { createServerClient, parseCookieHeader } from "@supabase/ssr";
import type { AstroCookies } from "astro";
const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL
const supabasePublishableKey = import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY
export function createClient({
request,
cookies,
}: {
request: Request;
cookies: AstroCookies;
}) {
return createServerClient(
supabaseUrl,
supabasePublishableKey,
{
cookies: {
getAll() {
return parseCookieHeader(
request.headers.get("Cookie") ?? ""
);
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
cookies.set(name, value, options)
);
},
},
}
);
}
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={7}>
<StepHikeCompact.Details title="Create authentication actions">
Create a new file at `src/actions/index.ts` to define server-side authentication actions for signing up, signing in, and signing out:
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```ts name=src/actions/index.ts
import { defineAction } from "astro:actions";
import { z } from "astro/zod";
import { createClient } from "../lib/supabase";
export const server = {
signUp: defineAction({
accept: "form",
input: z.object({
email: z.string().email(),
password: z.string().min(6),
}),
handler: async (input, context) => {
try {
const supabase = createClient({
request: context.request,
cookies: context.cookies,
});
const { error } = await supabase.auth.signUp({
email: input.email,
password: input.password,
options: {
emailRedirectTo: "http://localhost:4321/auth/callback",
},
});
if (error) {
return {
success: false,
message: error.message,
};
}
return {
success: true,
message: "Check your email to confirm your account",
};
} catch (err) {
return {
success: false,
message: "Unexpected error",
};
}
},
}),
signIn: defineAction({
accept: "form",
input: z.object({
email: z.string().email(),
password: z.string(),
}),
handler: async (input, context) => {
try {
const supabase = createClient({
request: context.request,
cookies: context.cookies,
});
const { error } = await supabase.auth.signInWithPassword({
email: input.email,
password: input.password,
});
if (error) {
return {
success: false,
message: error.message,
};
}
return {
success: true,
message: "Signed in successfully",
};
} catch (err) {
return {
success: false,
message: "Unexpected error",
};
}
},
}),
signOut: defineAction({
handler: async (_, context) => {
try {
const supabase = createClient({
request: context.request,
cookies: context.cookies,
});
await supabase.auth.signOut();
return {
success: true,
};
} catch (err) {
return {
success: false,
message: "Failed to sign out",
};
}
},
}),
};
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={8}>
<StepHikeCompact.Details title="Customize email template">
Before users can confirm their email, update the Supabase email template to send the token hash to your callback URL.
In your [Supabase project dashboard](/dashboard/project/_/auth/templates):
- Go to **Auth** > **Email Templates**
- Select the **Confirm signup** template
- Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=email`.
- Change your [Site URL](/dashboard/project/_/auth/url-configuration) to `http://localhost:4321`
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```html name=Email\ Template
{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=email
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={9}>
<StepHikeCompact.Details title="Create an auth callback page">
Create a new file at `src/pages/auth/callback.astro` to handle the email confirmation callback. Extract the token from the URL and verify it with Supabase:
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```astro name=src/pages/auth/callback.astro
---
import { createClient } from "../../lib/supabase";
import type { EmailOtpType } from "@supabase/supabase-js";
const supabase = createClient({
request: Astro.request,
cookies: Astro.cookies,
});
const requestUrl = new URL(Astro.request.url);
const token_hash = requestUrl.searchParams.get('token_hash');
const type = requestUrl.searchParams.get('type') as EmailOtpType | null;
if (token_hash && type) {
const { error } = await supabase.auth.verifyOtp({
token_hash,
type,
});
if (!error) {
return Astro.redirect("/dashboard");
}
}
return Astro.redirect("/auth/signin");
---
<html>
<head>
<title>Email Confirmation</title>
</head>
<body>
<p>Confirming your email...</p>
</body>
</html>
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={10}>
<StepHikeCompact.Details title="Create a sign-up page">
Create a new file at `src/pages/auth/signup.astro` with a sign-up form. Use a client-side event listener to handle form submission:
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```astro name=src/pages/auth/signup.astro
---
import { createClient } from "../../lib/supabase";
const supabase = createClient({
request: Astro.request,
cookies: Astro.cookies,
});
const { data } = await supabase.auth.getUser();
if (data?.user) {
return Astro.redirect("/dashboard");
}
---
<html>
<head>
<title>Sign Up</title>
</head>
<body>
<h1>Sign Up</h1>
<div id="message"></div>
<form id="signup-form">
<div>
<label for="email">Email</label>
<input
id="email"
type="email"
name="email"
placeholder="your@email.com"
required
/>
</div>
<div>
<label for="password">Password</label>
<input
id="password"
type="password"
name="password"
placeholder="At least 6 characters"
required
/>
</div>
<button type="submit" id="signup-btn">Sign Up</button>
</form>
<p>
Already have an account? <a href="/auth/signin">Sign in</a>
</p>
</body>
</html>
<script>
import { actions } from "astro:actions";
const form = document.querySelector("#signup-form") as HTMLFormElement;
const btn = document.getElementById("signup-btn") as HTMLButtonElement;
const messageEl = document.getElementById("message") as HTMLDivElement;
form?.addEventListener("submit", async (e) => {
e.preventDefault();
btn.disabled = true;
btn.textContent = "Signing up...";
messageEl.textContent = "";
try {
const formData = new FormData(form);
const result = await actions.signUp(formData);
if (!result.data?.success) {
btn.disabled = false;
btn.textContent = "Sign Up";
messageEl.textContent = result.data?.message || "Sign up failed";
messageEl.style.color = "red";
return;
}
messageEl.textContent = result.data.message;
messageEl.style.color = "green";
btn.textContent = "Sign Up";
} catch (error) {
btn.disabled = false;
btn.textContent = "Sign Up";
messageEl.textContent = "An error occurred. Please try again.";
messageEl.style.color = "red";
console.error(error);
}
});
</script>
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={11}>
<StepHikeCompact.Details title="Create a sign-in page">
Create a new file at `src/pages/auth/signin.astro` with a sign-in form. Use a client-side event listener to handle form submission:
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```astro name=src/pages/auth/signin.astro
---
import { createClient } from "../../lib/supabase";
const supabase = createClient({
request: Astro.request,
cookies: Astro.cookies,
});
const { data } = await supabase.auth.getUser();
if (data?.user) {
return Astro.redirect("/dashboard");
}
---
<html>
<head>
<title>Sign In</title>
</head>
<body>
<h1>Sign In</h1>
<div id="message"></div>
<form id="signin-form">
<div>
<label for="email">Email</label>
<input
id="email"
type="email"
name="email"
placeholder="your@email.com"
required
/>
</div>
<div>
<label for="password">Password</label>
<input
id="password"
type="password"
name="password"
placeholder="Your password"
required
/>
</div>
<button type="submit" id="signin-btn">Sign In</button>
</form>
<p>
Don't have an account? <a href="/auth/signup">Sign up</a>
</p>
</body>
</html>
<script>
import { actions } from "astro:actions";
const form = document.querySelector("#signin-form") as HTMLFormElement;
const btn = document.getElementById("signin-btn") as HTMLButtonElement;
const messageEl = document.getElementById("message") as HTMLDivElement;
form?.addEventListener("submit", async (e) => {
e.preventDefault();
btn.disabled = true;
btn.textContent = "Signing in...";
messageEl.textContent = "";
try {
const formData = new FormData(form);
const result = await actions.signIn(formData);
if (!result.data?.success) {
btn.disabled = false;
btn.textContent = "Sign In";
messageEl.textContent = result.data?.message || "Sign in failed";
messageEl.style.color = "red";
return;
}
// Redirect to dashboard on successful sign in
window.location.href = "/dashboard";
} catch (error) {
btn.disabled = false;
btn.textContent = "Sign In";
messageEl.textContent = "An error occurred. Please try again.";
messageEl.style.color = "red";
console.error(error);
}
});
</script>
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={12}>
<StepHikeCompact.Details title="Create a dashboard page">
Create a new file at `src/pages/dashboard.astro` to display the authenticated user's information. Use a client-side event listener for the sign-out button:
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```astro name=src/pages/dashboard.astro
---
import { createClient } from "../lib/supabase";
const supabase = createClient({
request: Astro.request,
cookies: Astro.cookies,
});
const { data } = await supabase.auth.getUser();
const user = data?.user;
if (!user) {
return Astro.redirect("/auth/signin");
}
---
<html>
<head>
<title>Dashboard</title>
</head>
<body>
<h1>Welcome!</h1>
<p>Email: {user.email}</p>
<p>User ID: {user.id}</p>
<button id="signout-btn">Sign Out</button>
</body>
</html>
<script>
import { actions } from "astro:actions";
const btn = document.getElementById("signout-btn") as HTMLButtonElement;
btn?.addEventListener("click", async (e) => {
e.preventDefault();
btn.disabled = true;
btn.textContent = "Signing out...";
try {
const result = await actions.signOut();
if (!result.data?.success) {
btn.disabled = false;
btn.textContent = "Sign Out";
alert("Failed to sign out");
return;
}
// Redirect to signin page
window.location.href = "/auth/signin";
} catch (error) {
btn.disabled = false;
btn.textContent = "Sign Out";
console.error(error);
}
});
</script>
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={13}>
<StepHikeCompact.Details title="Start the app">
Start the development server, then navigate to http://localhost:4321/auth/signup to test the authentication.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash name=Terminal
npm run dev
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>
## Learn more
- [Supabase Auth docs](/docs/guides/auth#authentication) for more Supabase authentication methods