From e88a3723e135e50d444f63cf2b8d4db115d4b68c Mon Sep 17 00:00:00 2001 From: Sean Oliver <882952+seanoliver@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:37:05 -0700 Subject: [PATCH 1/2] feat(studio): add PostHog session replay with masked-by-default policy (#48515) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Telemetry feature. ## What is the current behavior? - Session replay is off, and nothing in the code keeps it off. - `packages/common/posthog-client.ts` sets no recording config at all. - So PostHog's project setting alone decides, for every app sharing that project. - Studio, www and docs share one project. - Studio shows customer data almost everywhere: SQL editor, table rows, connection strings, API keys. - posthog-js masks inputs by default. It does not mask rendered text. - [GROWTH-1055](https://linear.app/supabase/issue/GROWTH-1055) ## What is the new behavior? - `posthogClient.init()` takes a masking config, and disables recording when it gets none. - Studio passes one behind `NEXT_PUBLIC_POSTHOG_SESSION_REPLAY`. - Every other app passes nothing, so it never loads the recorder. - Studio masks all text and all inputs. - `data-ph-capture="true"` opts one element's text back in. Unused so far. - Canvas is blocked, because it records as images that text masking cannot reach. - Query strings and fragments are stripped from recorded URLs, where auth callbacks carry tokens. - Request and response bodies are never recorded. - Console logs are never recorded, since masking only reaches DOM text. - Masking is set in code, so PostHog's settings cannot loosen it. - Consent gating is unchanged. Nothing records before a user accepts. ## Additional context - Recording needs three things: this env var, the PostHog project toggle, and user consent. - All three are off or unset, so merging this changes nothing at runtime. - `NEXT_PUBLIC_POSTHOG_SESSION_REPLAY` goes into Vercel on Preview scope first, to test on a preview build. - Production scope comes later, once we are ready to record there. - `NEXT_PUBLIC_*` is inlined at build time, so each scope needs a rebuild afterwards. - Text inside HTML attributes (`title`, `alt`, `href`) is still recorded as-is. - posthog-js exposes no hook for masking attributes, so covering it needs `ph-no-capture` per component. - Staging has no server-side masking config, so that is where this gets verified. - Plan: enable recording on staging, verify masked text on a preview, then decide on production. - Network timing stays on for the dashboard performance work. Payloads stay off. - Tests cover both masking functions and the config values. ## Screenshots https://github.com/user-attachments/assets/aa064a04-f977-4453-a3da-2fe0cdcead08 CleanShot 2026-07-31 at 10 13 43 ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Added privacy-focused session replay for Studio. * Text and form inputs are masked by default, with explicit opt-in capture. * Network recordings remove query strings and fragments. * Headers, request bodies, canvas data, and console logs are excluded. * **Bug Fixes** * Improved whitespace and capture-attribute handling during masking. * Session replay remains disabled without a masking policy or explicit enablement. --- apps/studio/lib/session-replay.test.ts | 162 +++++++++++++++++++++++++ apps/studio/lib/session-replay.ts | 50 ++++++++ apps/studio/lib/telemetry.tsx | 2 + packages/common/posthog-client.ts | 35 +++++- packages/common/telemetry.tsx | 23 +++- 5 files changed, 266 insertions(+), 6 deletions(-) create mode 100644 apps/studio/lib/session-replay.test.ts create mode 100644 apps/studio/lib/session-replay.ts diff --git a/apps/studio/lib/session-replay.test.ts b/apps/studio/lib/session-replay.test.ts new file mode 100644 index 0000000000000..eac2fa5864619 --- /dev/null +++ b/apps/studio/lib/session-replay.test.ts @@ -0,0 +1,162 @@ +import { buildSessionRecordingConfig, type CapturedNetworkRequest } from 'common' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { maskReplayNetworkRequest, maskReplayText, SESSION_REPLAY_CONFIG } from './session-replay' + +const elementWith = (attributes: Record) => { + const element = document.createElement('span') + Object.entries(attributes).forEach(([key, value]) => element.setAttribute(key, value)) + return element +} + +const networkRequest = (name: string): CapturedNetworkRequest => ({ + name, + entryType: 'resource', + startTime: 0, + duration: 0, +}) + +describe('maskReplayText', () => { + it('masks text by default', () => { + expect(maskReplayText('postgresql://postgres:hunter2@db.abc.supabase.co:5432')).toBe( + '*'.repeat('postgresql://postgres:hunter2@db.abc.supabase.co:5432'.length) + ) + }) + + it('masks text when no element is given', () => { + expect(maskReplayText('secret', undefined)).toBe('******') + }) + + it('masks based on trimmed length so whitespace is not leaked', () => { + expect(maskReplayText(' abc ')).toBe('***') + }) + + it('captures text opted in with data-ph-capture', () => { + const element = elementWith({ 'data-ph-capture': 'true' }) + expect(maskReplayText('Table editor', element)).toBe('Table editor') + }) + + it('masks text when data-ph-capture is not exactly "true"', () => { + expect(maskReplayText('secret', elementWith({ 'data-ph-capture': 'false' }))).toBe('******') + expect(maskReplayText('secret', elementWith({ 'data-ph-capture': '' }))).toBe('******') + expect(maskReplayText('secret', elementWith({ 'data-ph-capture': 'TRUE' }))).toBe('******') + }) + + it('masks text on elements carrying unrelated data attributes', () => { + expect(maskReplayText('secret', elementWith({ 'data-capture': 'true' }))).toBe('******') + }) +}) + +describe('maskReplayNetworkRequest', () => { + it('strips query strings', () => { + expect( + maskReplayNetworkRequest(networkRequest('https://api.supabase.com/v1/x?token=abc')).name + ).toBe('https://api.supabase.com/v1/x') + }) + + it('strips fragments, which carry GoTrue access tokens on auth callbacks', () => { + expect( + maskReplayNetworkRequest(networkRequest('https://supabase.com/dashboard#access_token=abc')) + .name + ).toBe('https://supabase.com/dashboard') + }) + + it('strips from the first separator when both are present', () => { + expect(maskReplayNetworkRequest(networkRequest('https://x.com/a?b=1#c=2')).name).toBe( + 'https://x.com/a' + ) + expect(maskReplayNetworkRequest(networkRequest('https://x.com/a#c=2?b=1')).name).toBe( + 'https://x.com/a' + ) + }) + + it('leaves URLs without a query string or fragment alone', () => { + expect(maskReplayNetworkRequest(networkRequest('https://x.com/project/abc/editor')).name).toBe( + 'https://x.com/project/abc/editor' + ) + }) + + it('returns the request rather than dropping it, so timings are still captured', () => { + const request = networkRequest('https://x.com/a?b=1') + expect(maskReplayNetworkRequest(request)).toBe(request) + }) +}) + +describe('SESSION_REPLAY_CONFIG', () => { + it('masks all text and inputs', () => { + expect(SESSION_REPLAY_CONFIG.maskTextSelector).toBe('*') + expect(SESSION_REPLAY_CONFIG.maskAllInputs).toBe(true) + expect(SESSION_REPLAY_CONFIG.maskTextFn).toBe(maskReplayText) + }) + + it('never records request or response payloads', () => { + expect(SESSION_REPLAY_CONFIG.recordHeaders).toBe(false) + expect(SESSION_REPLAY_CONFIG.recordBody).toBe(false) + }) + + it('never records canvas, which text masking cannot reach', () => { + expect(SESSION_REPLAY_CONFIG.captureCanvas).toEqual({ recordCanvas: false }) + }) + + it('strips sensitive URL parts via maskReplayNetworkRequest', () => { + expect(SESSION_REPLAY_CONFIG.maskCapturedNetworkRequestFn).toBe(maskReplayNetworkRequest) + }) +}) + +describe('buildSessionRecordingConfig', () => { + it('disables recording when given no policy', () => { + const config = buildSessionRecordingConfig() + + expect(config.disable_session_recording).toBe(true) + expect(config).not.toHaveProperty('session_recording') + }) + + it('disables recording when the policy is undefined', () => { + const config = buildSessionRecordingConfig(undefined) + + expect(config.disable_session_recording).toBe(true) + expect(config).not.toHaveProperty('session_recording') + }) + + it('enables recording and forwards the policy when given one', () => { + const config = buildSessionRecordingConfig(SESSION_REPLAY_CONFIG) + + expect(config.disable_session_recording).toBe(false) + expect(config.session_recording).toBe(SESSION_REPLAY_CONFIG) + }) + + it.each([undefined, SESSION_REPLAY_CONFIG])( + 'never records console logs, which masking cannot reach (%#)', + (sessionReplay) => { + expect(buildSessionRecordingConfig(sessionReplay).enable_recording_console_log).toBe(false) + } + ) +}) + +describe('IS_SESSION_REPLAY_ENABLED', () => { + beforeEach(() => { + vi.resetModules() + }) + + afterEach(() => { + vi.unstubAllEnvs() + }) + + it('is true only for the exact string "true"', async () => { + vi.stubEnv('NEXT_PUBLIC_POSTHOG_SESSION_REPLAY', 'true') + const { IS_SESSION_REPLAY_ENABLED } = await import('./session-replay') + expect(IS_SESSION_REPLAY_ENABLED).toBe(true) + }) + + it.each(['false', '', 'TRUE', '1'])('is false for %o', async (value) => { + vi.stubEnv('NEXT_PUBLIC_POSTHOG_SESSION_REPLAY', value) + const { IS_SESSION_REPLAY_ENABLED } = await import('./session-replay') + expect(IS_SESSION_REPLAY_ENABLED).toBe(false) + }) + + it('is false when unset', async () => { + vi.stubEnv('NEXT_PUBLIC_POSTHOG_SESSION_REPLAY', undefined) + const { IS_SESSION_REPLAY_ENABLED } = await import('./session-replay') + expect(IS_SESSION_REPLAY_ENABLED).toBe(false) + }) +}) diff --git a/apps/studio/lib/session-replay.ts b/apps/studio/lib/session-replay.ts new file mode 100644 index 0000000000000..db7283d1dee75 --- /dev/null +++ b/apps/studio/lib/session-replay.ts @@ -0,0 +1,50 @@ +import type { CapturedNetworkRequest, SessionRecordingOptions } from 'common' + +/** + * Enables session replay in Studio. Recording also requires "Record user + * sessions" in PostHog, which www and docs share. + */ +export const IS_SESSION_REPLAY_ENABLED = process.env.NEXT_PUBLIC_POSTHOG_SESSION_REPLAY === 'true' + +/** + * Setting `data-ph-capture="true"` on an element opts its text in to session + * recording. All text is opted out by default. + */ +const CAPTURE_DATASET_KEY = 'phCapture' + +/** + * Returns asterisks for all text except text inside elements marked + * `data-ph-capture="true"`. + */ +export function maskReplayText(text: string, element?: HTMLElement): string { + if (element?.dataset[CAPTURE_DATASET_KEY] === 'true') return text + return '*'.repeat(text.trim().length) +} + +/** + * Strips query strings and fragments from recorded URLs, which posthog-js applies + * to page URLs as well as network requests. Auth callbacks carry tokens in the + * fragment. + */ +export function maskReplayNetworkRequest(request: CapturedNetworkRequest): CapturedNetworkRequest { + if (request.name) { + const separatorIndex = request.name.search(/[?#]/) + if (separatorIndex !== -1) { + request.name = request.name.slice(0, separatorIndex) + } + } + return request +} + +export const SESSION_REPLAY_CONFIG: SessionRecordingOptions = { + // Match posthog-js defaults, but set here so the PostHog UI can't relax them. + maskAllInputs: true, + maskTextSelector: '*', + maskTextFn: maskReplayText, + // Keeps network capture to URL, status and timing. Overrides the PostHog UI. + recordHeaders: false, + recordBody: false, + // Canvas is captured as images, which text masking can't reach. + captureCanvas: { recordCanvas: false }, + maskCapturedNetworkRequestFn: maskReplayNetworkRequest, +} diff --git a/apps/studio/lib/telemetry.tsx b/apps/studio/lib/telemetry.tsx index cdf1a5b1c0011..cd7f53762a1f0 100644 --- a/apps/studio/lib/telemetry.tsx +++ b/apps/studio/lib/telemetry.tsx @@ -6,6 +6,7 @@ import { useConsentToast } from 'ui-patterns/consent' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { API_URL, IS_PLATFORM } from '@/lib/constants' +import { IS_SESSION_REPLAY_ENABLED, SESSION_REPLAY_CONFIG } from '@/lib/session-replay' export function Telemetry() { // Although this is "technically" breaking the rules of hooks @@ -70,6 +71,7 @@ export function Telemetry() { hasAcceptedConsent={hasAcceptedConsent} enabled={IS_PLATFORM} organizationSlug={organization?.slug} + sessionReplay={IS_SESSION_REPLAY_ENABLED ? SESSION_REPLAY_CONFIG : undefined} /> ) } diff --git a/packages/common/posthog-client.ts b/packages/common/posthog-client.ts index 9374535025c77..19dad1f57f79c 100644 --- a/packages/common/posthog-client.ts +++ b/packages/common/posthog-client.ts @@ -1,7 +1,13 @@ -import posthog, { PostHogConfig } from 'posthog-js' +import posthog, { + type CapturedNetworkRequest, + type PostHogConfig, + type SessionRecordingOptions, +} from 'posthog-js' import { safeSessionStorage } from './safe-storage' +export type { CapturedNetworkRequest, SessionRecordingOptions } + // Limit the max number of queued events // (e.g. if a user navigates around a lot before accepting consent) const MAX_PENDING_EVENTS = 20 @@ -23,6 +29,30 @@ interface PostHogClientConfig { uiHost?: string } +interface PostHogInitOptions { + hasConsent?: boolean + /** + * Masking policy for session replay. Omit to disable recording, which every app + * sharing this PostHog project does unless it passes a policy of its own. + */ + sessionReplay?: SessionRecordingOptions +} + +/** + * Enables session recording when given a masking config, and disables it when + * given nothing. + */ +export function buildSessionRecordingConfig( + sessionReplay?: SessionRecordingOptions +): Partial { + return { + disable_session_recording: !sessionReplay, + // Console output is not in the DOM, so text masking cannot reach it. + enable_recording_console_log: false, + ...(sessionReplay && { session_recording: sessionReplay }), + } +} + class PostHogClient { /** True after posthog.init() is called (prevents double-init) */ private initStarted = false @@ -50,7 +80,7 @@ class PostHogClient { } } - init(hasConsent: boolean = true) { + init({ hasConsent = true, sessionReplay }: PostHogInitOptions = {}) { if (this.initStarted || typeof window === 'undefined' || !hasConsent) return if (!this.config.apiKey) { @@ -64,6 +94,7 @@ class PostHogClient { autocapture: false, // We'll manually track events capture_pageview: false, // We'll manually track pageviews capture_pageleave: false, // We'll manually track page leaves + ...buildSessionRecordingConfig(sessionReplay), loaded: (posthog) => { // Apply pending properties that were set before PostHog // initialized due to poor connection or user not accepting diff --git a/packages/common/telemetry.tsx b/packages/common/telemetry.tsx index b92aae494cca1..4e8e13aa9f897 100644 --- a/packages/common/telemetry.tsx +++ b/packages/common/telemetry.tsx @@ -21,7 +21,13 @@ import { } from './first-referrer-cookie' import { ensurePlatformSuffix, isBrowser } from './helpers' import { useFirstTouchStore, useParams } from './hooks' -import { posthogClient, type ClientTelemetryEvent } from './posthog-client' +import { + buildSessionRecordingConfig, + posthogClient, + type CapturedNetworkRequest, + type ClientTelemetryEvent, + type SessionRecordingOptions, +} from './posthog-client' import { TelemetryEvent } from './telemetry-constants' import { clearFirstTouchData, @@ -30,7 +36,13 @@ import { } from './telemetry-first-touch-store' import { getSharedTelemetryData, getTelemetryCookieOptions } from './telemetry-utils' -export { posthogClient, type ClientTelemetryEvent } +export { + buildSessionRecordingConfig, + posthogClient, + type CapturedNetworkRequest, + type ClientTelemetryEvent, + type SessionRecordingOptions, +} export const TelemetryTagManager = () => { const { hasAccepted } = useConsentState() @@ -255,12 +267,15 @@ export const PageTelemetry = ({ enabled = true, organizationSlug, projectRef, + sessionReplay, }: { API_URL: string hasAcceptedConsent: boolean enabled?: boolean organizationSlug?: string projectRef?: string + /** Masking policy for session replay. Omit to disable recording. */ + sessionReplay?: SessionRecordingOptions }) => { const router = useRouter() @@ -315,9 +330,9 @@ export const PageTelemetry = ({ useEffect(() => { if (hasAcceptedConsent && IS_PLATFORM) { - posthogClient.init(true) + posthogClient.init({ sessionReplay }) } - }, [hasAcceptedConsent, IS_PLATFORM]) + }, [hasAcceptedConsent, IS_PLATFORM, sessionReplay]) // Waiting for router.isReady before sending to avoid dynamic route placeholders useEffect(() => { From 3a3661019f4f7dd79a5941bc379741bf45396047 Mon Sep 17 00:00:00 2001 From: Nik Richers Date: Fri, 31 Jul 2026 16:00:47 -0700 Subject: [PATCH 2/2] docs: update architecture diagram and references from Kong to Envoy (#48557) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? This is a docs update. The shared architecture diagram and several docs pages still described Kong as Supabase's API gateway, even though the hosted platform has run Envoy since 2025. Both diagram variants are rebuilt with real, accessible text — the originals rendered every label as an outlined vector path with zero `` elements — so the gateway name can be kept current going forward, and the platform-facing prose that named Kong directly is updated to Envoy. Closes DOCS-1262. ## What is the current behavior? - The architecture diagram (used on the Architecture overview, Auth architecture, Self-hosting Docker, and Contributing guide pages) shows "KONG / docs.konghq.com" as the gateway box - The Architecture overview page has a "Kong (API gateway)" component section - The Auth architecture page states "Kong API gateway. This is shared between all Supabase products." - `README.md` and `apps/docs/public/humans.txt` credit Kong instead of Envoy ## What is the new behavior? - Rebuilt `supabase-architecture.svg` and `supabase-architecture--light.svg` with real `` elements; the gateway box now reads "ENVOY / envoyproxy.io" with identical layout, colors, and shadows otherwise - Updated the diagram alt text and the "Kong (API gateway)" section (now "Envoy (API gateway)", with the correct docs link, license, and language) on the Architecture overview page - Updated the "Kong API gateway" bullet and diagram alt text on the Auth architecture page - Updated the Kong credit to Envoy in `README.md` and `apps/docs/public/humans.txt` **Intentionally excluded:** - Self-hosted Docker Compose pages (`docker.mdx`, `enable-mcp.mdx`, `self-hosted-auth-keys.mdx`, `self-hosted-envoy.mdx`, `self-hosted-functions.mdx`, `self-hosted-proxy-https.mdx`) — these describe the self-hosted stack, which still defaults to Kong today and is already owned by an open PR (#48153) that flips that default - `i18n/README.*.md` (29 files) — translation risk without native-speaker review; only the English `README.md` was updated ## Open questions - [ ] #48153 merges and the self-hosted default actually flips to Envoy — once it does, revisit the self-hosting Docker Compose pages excluded from this PR and the self-hosting-analytics reference TODO - [ ] Confirm whether all legacy platform instances have fully migrated to Envoy — until then, this PR's wording says "Envoy" without claiming Kong is gone everywhere (some legacy instances may still silently be on Kong) - [ ] Current Envoy response header names confirmed for the logs guide TODO (`x-kong-proxy-latency` / `x-kong-upstream-latency`) - [ ] i18n README translations (29 files) follow up separately with native-speaker review ## Additional context - Verification: rendered both new SVGs with `rsvg-convert` and visually diffed against the originals — layout, spacing, colors, and shadows are pixel-equivalent; only the top-box label text changed | Check | Result | | --- | --- | | `rsvg-convert` render, dark variant | pass — diagram unchanged except gateway label | | `rsvg-convert` render, light variant | pass — diagram unchanged except gateway label | | Preview URL, Architecture overview | pass — 200 | | Preview URL, Auth architecture | pass — 200 | ### Before & After #### [Architecture overview](https://supabase.com/docs/guides/getting-started/architecture) | [Before (production)](https://supabase.com/docs/guides/getting-started/architecture) | [After (PR preview)](https://docs-git-nikrichers-docs-1262-architecture-docs-84e339-supabase.vercel.app/docs/guides/getting-started/architecture) | | --- | --- | | ![Before](https://moijyfpvgnmgoxvwcikq.supabase.co/storage/v1/object/public/pr-proof/supabase/supabase/pr48557/getting-started-before-crop-a67d5681.png) | ![After](https://moijyfpvgnmgoxvwcikq.supabase.co/storage/v1/object/public/pr-proof/supabase/supabase/pr48557/getting-started-after-crop-7d2b027a.png) | #### [Auth architecture](https://supabase.com/docs/guides/auth/architecture) | [Before (production)](https://supabase.com/docs/guides/auth/architecture) | [After (PR preview)](https://docs-git-nikrichers-docs-1262-architecture-docs-84e339-supabase.vercel.app/docs/guides/auth/architecture) | | --- | --- | | ![Before](https://moijyfpvgnmgoxvwcikq.supabase.co/storage/v1/object/public/pr-proof/supabase/supabase/pr48557/auth-before-c68a7267.png) | ![After](https://moijyfpvgnmgoxvwcikq.supabase.co/storage/v1/object/public/pr-proof/supabase/supabase/pr48557/auth-after-3de5c4c5.png) | ### Test plan - [ ] Diagram renders correctly in both light and dark mode on the preview - [ ] "Envoy (API gateway)" section reads correctly on the Architecture overview page - [ ] Auth architecture bullet reads "Envoy API gateway" - [ ] The two TODO-marked follow-ups (logs guide, self-hosting-analytics) are acceptable to leave for later rather than block this PR --------- Co-authored-by: Nik Richers Co-authored-by: Miranda Limonczenko --- README.md | 2 +- .../docs/content/guides/auth/architecture.mdx | 4 +- .../guides/getting-started/architecture.mdx | 14 ++--- apps/docs/public/humans.txt | 2 +- .../img/supabase-architecture--light.svg | 51 +++++++++++++------ .../docs/public/img/supabase-architecture.svg | 51 +++++++++++++------ 6 files changed, 81 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 08120545543e0..0b5a5e6f4556c 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and - [Storage](https://github.com/supabase/storage-api) a RESTful API for managing files in S3, with Postgres handling permissions. - [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API. - [postgres-meta](https://github.com/supabase/postgres-meta) is a RESTful API for managing your Postgres, allowing you to fetch tables, add roles, and run queries, etc. -- [Kong](https://github.com/Kong/kong) is a cloud-native API gateway. +- [Envoy](https://github.com/envoyproxy/envoy) is a cloud-native, high-performance edge and service proxy. #### Client libraries diff --git a/apps/docs/content/guides/auth/architecture.mdx b/apps/docs/content/guides/auth/architecture.mdx index b4c0a53092b64..3665526c40c12 100644 --- a/apps/docs/content/guides/auth/architecture.mdx +++ b/apps/docs/content/guides/auth/architecture.mdx @@ -6,12 +6,12 @@ subtitle: 'The architecture behind Supabase Auth.' There are four major layers to Supabase Auth: 1. [Client layer.](#client-layer) This can be one of the Supabase client SDKs, or manually made HTTP requests using the HTTP client of your choice. -1. Kong API gateway. This is shared between all Supabase products. +1. Envoy API gateway. This is shared between all Supabase products. 1. [Auth service](#auth-service) (formerly known as GoTrue). 1. [Postgres database.](#postgres) This is shared between all Supabase products. Diagram showing the architecture of Supabase. The Kong API gateway sits in front of 7 services: GoTrue, PostgREST, Realtime, Storage, pg_meta, Functions, and pg_graphql. All the services talk to a single Postgres instance. - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -171,4 +171,23 @@ + +ENVOY +envoyproxy.io +POSTGRESQL +postgresql.org +GoTrue +PostgREST +Realtime +Storage +pg-meta +Functions +pg_graphql +/auth +/rest +/realtime +/storage +/pg +/functions +/graphql diff --git a/apps/docs/public/img/supabase-architecture.svg b/apps/docs/public/img/supabase-architecture.svg index 2b0ad6325a99a..6b4dfd3dc8f15 100644 --- a/apps/docs/public/img/supabase-architecture.svg +++ b/apps/docs/public/img/supabase-architecture.svg @@ -15,69 +15,69 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -171,4 +171,23 @@ + +ENVOY +envoyproxy.io +POSTGRESQL +postgresql.org +GoTrue +PostgREST +Realtime +Storage +pg-meta +Functions +pg_graphql +/auth +/rest +/realtime +/storage +/pg +/functions +/graphql