diff --git a/.github/workflows/www-tests.yml b/.github/workflows/www-tests.yml
index a29e1b4765c..96f2737cccf 100644
--- a/.github/workflows/www-tests.yml
+++ b/.github/workflows/www-tests.yml
@@ -10,6 +10,7 @@ on:
- 'apps/www/lib/**/*.js'
- 'apps/www/content/md/**'
- 'apps/www/scripts/**/*.mjs'
+ - 'apps/www/public/.well-known/**'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
diff --git a/apps/www/app/(home)/page.tsx b/apps/www/app/(home)/page.tsx
index 8aa34bd438c..d9c2094bff7 100644
--- a/apps/www/app/(home)/page.tsx
+++ b/apps/www/app/(home)/page.tsx
@@ -2,7 +2,13 @@ import type { Metadata } from 'next'
import { FrameworksSection } from './_components/FrameworksSection'
import { HomeContent } from './_components/HomeContent'
-import { organizationSchema, serializeJsonLd, websiteSchema } from '@/lib/json-ld'
+import { DEFAULT_META_DESCRIPTION } from '@/lib/constants'
+import {
+ organizationSchema,
+ serializeJsonLd,
+ softwareApplicationSchema,
+ websiteSchema,
+} from '@/lib/json-ld'
import { mdAlternates } from '@/lib/md-alternates'
export const metadata: Metadata = {
@@ -23,6 +29,19 @@ export default function HomePage() {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: serializeJsonLd(websiteSchema()) }}
/>
+
} />
>
)
diff --git a/apps/www/ard-catalog.test.ts b/apps/www/ard-catalog.test.ts
new file mode 100644
index 00000000000..92ef005e68a
--- /dev/null
+++ b/apps/www/ard-catalog.test.ts
@@ -0,0 +1,80 @@
+import { existsSync, promises as fs } from 'node:fs'
+import path from 'node:path'
+import { describe, expect, it } from 'vitest'
+
+import rewrites from './lib/rewrites'
+
+const CANONICAL_ORIGIN = 'https://supabase.com'
+
+const NOT_AGENT_RESOURCES: Record = {
+ 'ard.json': 'the catalog itself',
+ 'api-catalog':
+ 'peer catalog (RFC 9727); its resource, the Management API spec, has its own entry',
+ 'ai-catalog.json': 'legacy ARD alias, rewritten to ard.json',
+ 'mcp-registry-auth': 'domain-ownership verification token',
+ 'openai-apps-challenge': 'domain-ownership verification token',
+ 'security.txt': 'vulnerability disclosure contact',
+ vercel: 'Vercel toolbar dev tooling',
+}
+
+type ArdEntry = { identifier: string; url: string }
+
+async function loadArdCatalog(): Promise<{ entries: ArdEntry[] }> {
+ const raw = await fs.readFile(
+ path.join(process.cwd(), 'public', '.well-known', 'ard.json'),
+ 'utf-8'
+ )
+ return JSON.parse(raw)
+}
+
+function wellKnownRewriteSources(): string[] {
+ return rewrites
+ .map((rewrite: { source: string }) => rewrite.source)
+ .filter((source: string) => source.startsWith('/.well-known/'))
+ .map((source: string) => source.replace('/.well-known/', ''))
+}
+
+describe('agent discovery catalog (.well-known/ard.json)', () => {
+ it('every .well-known surface is cataloged or explicitly marked as not an agent resource', async () => {
+ const { entries } = await loadArdCatalog()
+ const catalogedUrls = entries.map((entry) => entry.url)
+
+ const wellKnownDir = path.join(process.cwd(), 'public', '.well-known')
+ const dirents = await fs.readdir(wellKnownDir, { withFileTypes: true })
+ const surfaces = [...dirents.map((dirent) => dirent.name), ...wellKnownRewriteSources()]
+
+ expect(surfaces.length).toBeGreaterThan(0)
+
+ for (const surface of surfaces) {
+ if (surface in NOT_AGENT_RESOURCES) continue
+ const cataloged = catalogedUrls.some((url) =>
+ url.startsWith(`${CANONICAL_ORIGIN}/.well-known/${surface}`)
+ )
+ expect(
+ cataloged,
+ `new .well-known surface "${surface}": add an entry for it to public/.well-known/ard.json, or add it to NOT_AGENT_RESOURCES in this test with the reason it doesn't belong in the catalog`
+ ).toBe(true)
+ }
+ })
+
+ it('every same-origin catalog entry resolves to a public file, app route, or rewrite', async () => {
+ const { entries } = await loadArdCatalog()
+ expect(entries.length).toBeGreaterThan(0)
+
+ const rewriteSources = rewrites.map((rewrite: { source: string }) => rewrite.source)
+
+ for (const entry of entries) {
+ const url = new URL(entry.url)
+ if (url.origin !== CANONICAL_ORIGIN) continue
+
+ const publicFile = path.join(process.cwd(), 'public', url.pathname)
+ const appRoute = path.join(process.cwd(), 'app', url.pathname, 'route.ts')
+ const resolves =
+ existsSync(publicFile) || existsSync(appRoute) || rewriteSources.includes(url.pathname)
+ expect(
+ resolves,
+ `ard.json entry "${entry.identifier}" points at ${entry.url}, but ${url.pathname} is not a file in public/, an app route, or a rewrite source — the catalog is advertising a dead URL`
+ ).toBe(true)
+ }
+ })
+})
diff --git a/apps/www/lib/json-ld.ts b/apps/www/lib/json-ld.ts
index 3b3ead15f3e..198980847e1 100644
--- a/apps/www/lib/json-ld.ts
+++ b/apps/www/lib/json-ld.ts
@@ -41,6 +41,19 @@ export function organizationSchema(input: OrganizationSchemaInput = {}) {
url: ORG_LOGO_URL,
},
description: input.description ?? DEFAULT_META_DESCRIPTION,
+ legalName: 'Supabase Pte. Ltd.',
+ contactPoint: {
+ '@type': 'ContactPoint',
+ contactType: 'customer support',
+ url: `${CANONICAL_ORIGIN}/support`,
+ },
+ address: {
+ '@type': 'PostalAddress',
+ streetAddress: '65 Chulia Street #38-02/03, OCBC Centre',
+ addressLocality: 'Singapore',
+ postalCode: '049513',
+ addressCountry: 'SG',
+ },
sameAs: ORG_SAMEAS,
}
}
diff --git a/apps/www/lib/rewrites.js b/apps/www/lib/rewrites.js
index fbd4339024d..c11c0129b13 100644
--- a/apps/www/lib/rewrites.js
+++ b/apps/www/lib/rewrites.js
@@ -75,6 +75,11 @@ const rewrites = [
source: '/.well-known/security.txt',
destination: `${process.env.NEXT_PUBLIC_DOCS_URL}/.well-known/security.txt`,
},
+ {
+ // legacy AI Catalog path; /.well-known/ard.json is the canonical source
+ source: '/.well-known/ai-catalog.json',
+ destination: '/.well-known/ard.json',
+ },
{
source: '/openapi.json',
destination: 'https://api.supabase.com/api/v1-json',
diff --git a/apps/www/public/.well-known/ard.json b/apps/www/public/.well-known/ard.json
new file mode 100644
index 00000000000..0172ac91f73
--- /dev/null
+++ b/apps/www/public/.well-known/ard.json
@@ -0,0 +1,72 @@
+{
+ "specVersion": "1.0",
+ "host": {
+ "displayName": "Supabase",
+ "identifier": "https://supabase.com",
+ "documentationUrl": "https://supabase.com/docs"
+ },
+ "entries": [
+ {
+ "identifier": "urn:air:supabase.com:mcp:server",
+ "displayName": "Supabase MCP server",
+ "type": "application/json",
+ "url": "https://mcp.supabase.com/mcp",
+ "description": "The Supabase MCP server endpoint (streamable HTTP, OAuth-protected) for managing projects, database schema, and queries from MCP clients.",
+ "tags": ["mcp", "server", "agents"],
+ "representativeQueries": [
+ "connect my agent to Supabase over MCP",
+ "run a query against my Supabase project from an MCP client",
+ "manage my Supabase project from my coding agent"
+ ]
+ },
+ {
+ "identifier": "urn:air:supabase.com:metadata:mcp-oauth",
+ "displayName": "Supabase MCP OAuth metadata",
+ "type": "application/json",
+ "url": "https://api.supabase.com/.well-known/oauth-protected-resource/mcp",
+ "description": "OAuth protected-resource metadata for connecting clients to the Supabase MCP server.",
+ "tags": ["mcp", "oauth", "authorization", "metadata"],
+ "representativeQueries": [
+ "authenticate an MCP client to the Supabase MCP server with OAuth",
+ "what OAuth scopes does the Supabase MCP server support"
+ ]
+ },
+ {
+ "identifier": "urn:air:supabase.com:api:management",
+ "displayName": "Supabase Management API",
+ "type": "application/openapi+json",
+ "url": "https://api.supabase.com/api/v1-json",
+ "description": "OpenAPI 3.0 description of the Supabase Management API for managing organizations, projects, branches, and configuration.",
+ "tags": ["api", "openapi", "projects", "management"],
+ "representativeQueries": [
+ "create a new Supabase project",
+ "list the branches of my Supabase project",
+ "update the Postgres config of a Supabase project"
+ ]
+ },
+ {
+ "identifier": "urn:air:supabase.com:docs:llms-txt",
+ "displayName": "Supabase llms.txt",
+ "type": "text/plain",
+ "url": "https://supabase.com/llms.txt",
+ "description": "Index of Supabase documentation and content available as agent-readable markdown.",
+ "tags": ["documentation", "llms", "markdown"],
+ "representativeQueries": [
+ "how do I set up Supabase auth in Next.js",
+ "Supabase Row Level Security documentation"
+ ]
+ },
+ {
+ "identifier": "urn:air:supabase.com:skills:index",
+ "displayName": "Supabase Agent Skills",
+ "type": "application/json",
+ "url": "https://supabase.com/.well-known/agent-skills/index.json",
+ "description": "Agent Skills discovery index for installable Supabase and Postgres skills.",
+ "tags": ["skills", "agents", "postgres"],
+ "representativeQueries": [
+ "write RLS policies for my Supabase tables",
+ "create a Postgres migration for my Supabase project"
+ ]
+ }
+ ]
+}