diff --git a/app/_meta.ts b/app/_meta.ts index 8734c6d1..32c9f01c 100644 --- a/app/_meta.ts +++ b/app/_meta.ts @@ -33,4 +33,7 @@ export default { changes: { display: "hidden", }, + review: { + display: "hidden", + }, } satisfies MetaRecord; diff --git a/app/authzed/concepts/audit-logging/page.mdx b/app/authzed/concepts/audit-logging/page.mdx index 2ad53d6f..694d2ae6 100644 --- a/app/authzed/concepts/audit-logging/page.mdx +++ b/app/authzed/concepts/audit-logging/page.mdx @@ -7,7 +7,8 @@ import { Callout, Tabs } from "nextra/components"; # Audit Logging -Audit Logging is functionality exclusive to AuthZed products that publishes logs of SpiceDB API operations to a log sink. +Audit Logging publishes logs of SpiceDB API operations to a log sink. +It ships with AuthZed Cloud, Dedicated, and Enterprise; SpiceDB open source does not include it. ## Log Format @@ -171,25 +172,31 @@ The process for setting up audit logging varies depending on the AuthZed product Using the web dashboard, navigate to the Permission System's settings page to find the Audit Log settings. -### Self-Hosted +### SpiceDB Enterprise (self-hosted) [#self-hosted] + + + **AuthZed Enterprise only.** The `--extender-audit-*` flags below are flags on `spicedb serve` in + the AuthZed-licensed SpiceDB Enterprise binary. They are not present in [SpiceDB open + source](/authzed/guides/picking-a-product#open-source), which has no audit logging. + Use the following command-line flags: -| Flag | Description | Default | -| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| --extender-enabled | must be set to`authzed-audit` | | -| --extender-audit-batch-size-limit | defines the maximum number of audit events to be processed as a unit | `10000` | -| --extender-audit-buffer-size | defines the size of the audit log buffer that holds events to be processed by workers | `1000000` | -| --extender-audit-buffer-window | defines maximum amount of time events are buffered before being pushed | `1s` | -| --extender-audit-disabled-on-methods strings | list of comma-separated, fully-qualified API methods to disable events for. Watch API is always excluded (e.g. `/authzed.api.v1.PermissionsService/CheckPermission`) | | -| --extender-audit-initial-retry-interval duration | sets the first retry backoff in case of a failure to push audit events to the backend | `1s` | -| --extender-audit-max-retry-interval duration | sets the maximum backoff duration in case of failure to push events | `30s` | -| --extender-audit-retry-randomizer-factor | sets the randomization factor for the backoff duration - this helps prevent thundering herds on event push errors | `0.5` | -| --extender-audit-stream-name | defines the name of the target stream/topic (e.g. Kafka Topic, Kinesis Stream...) | `spicedb` | -| --extender-audit-target-configuration | target-type specific configuration | `[]` | -| --extender-audit-target-endpoint-url string | defines the URL of target endpoint to ingest audit events. If left unspecified, some types will try to determine automatically (e.g. AWS SDK) | | -| --extender-audit-target-type | defines the type of target to ingest audit events | `noop` | -| --extender-audit-worker-count | defines the number of worker goroutines to process audit events (default 5) | | +| Flag | Description | Default | +| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `--extender-enabled` | must be set to `authzed-audit` | | +| `--extender-audit-batch-size-limit` | defines the maximum number of audit events to be processed as a unit | `10000` | +| `--extender-audit-buffer-size` | defines the size of the audit log buffer that holds events to be processed by workers | `1000000` | +| `--extender-audit-buffer-window` | defines maximum amount of time events are buffered before being pushed | `1s` | +| `--extender-audit-disabled-on-methods` strings | list of comma-separated, fully-qualified API methods to disable events for. Watch API is always excluded (e.g. `/authzed.api.v1.PermissionsService/CheckPermission`) | | +| `--extender-audit-initial-retry-interval` duration | sets the first retry backoff in case of a failure to push audit events to the backend | `1s` | +| `--extender-audit-max-retry-interval` duration | sets the maximum backoff duration in case of failure to push events | `30s` | +| `--extender-audit-retry-randomizer-factor` | sets the randomization factor for the backoff duration - this helps prevent thundering herds on event push errors | `0.5` | +| `--extender-audit-stream-name` | defines the name of the target stream/topic (e.g. Kafka Topic, Kinesis Stream...) | `spicedb` | +| `--extender-audit-target-configuration` | target-type specific configuration | `[]` | +| `--extender-audit-target-endpoint-url` string | defines the URL of target endpoint to ingest audit events. If left unspecified, some types will try to determine automatically (e.g. AWS SDK) | | +| `--extender-audit-target-type` | defines the type of target to ingest audit events | `noop` | +| `--extender-audit-worker-count` | defines the number of worker goroutines to process audit events (default 5) | | ### Tokens in Audit Logs diff --git a/app/authzed/concepts/private-networking/page.mdx b/app/authzed/concepts/private-networking/page.mdx index dc8e7d69..796e78f6 100644 --- a/app/authzed/concepts/private-networking/page.mdx +++ b/app/authzed/concepts/private-networking/page.mdx @@ -7,7 +7,7 @@ import { Callout } from "nextra/components"; # Private Networking -Private Networking is functionality exclusive to AuthZed Dedicated that restricts network access to internal networks. +Private Networking restricts network access to internal networks. When enabled, connections that are not configured are rejected. By adding this additional layer of security, entire classes of security risk are eliminated because only trusted networks have access to the software powering your authorization systems. diff --git a/app/authzed/concepts/restricted-api-access/page.mdx b/app/authzed/concepts/restricted-api-access/page.mdx index 150b0037..d24dd05c 100644 --- a/app/authzed/concepts/restricted-api-access/page.mdx +++ b/app/authzed/concepts/restricted-api-access/page.mdx @@ -7,7 +7,8 @@ import { Callout, Tabs } from "nextra/components"; # Restricted API Access -Restricted API Access is functionality exclusive to AuthZed products that restricts access to SpiceDB for API Tokens. +Restricted API Access restricts access to SpiceDB for API Tokens. +It ships with AuthZed Cloud, Dedicated, and Enterprise; SpiceDB open source does not include it. This functionality enables organizations to apply the principle of least-privilege to services accessing SpiceDB. For example, read-only tokens can be created for services that should never need to write to SpiceDB. @@ -209,15 +210,31 @@ The process for setting up this feature varies depending on the AuthZed product Using the web dashboard, navigate to the Permission System's "Access" tab. -### Self-Hosted +### SpiceDB Enterprise (self-hosted) [#self-hosted] + + + **AuthZed Enterprise only.** The `--extender-*` flags below are flags on `spicedb serve` in the + AuthZed-licensed SpiceDB Enterprise binary. They are not present in [SpiceDB open + source](/authzed/guides/picking-a-product#open-source), so passing them to the open source + `spicedb` fails with an unknown-flag error. + + +For example: + +```sh +spicedb serve \ + --grpc-preshared-key "" \ + --extender-enabled authzed-fgam \ + --extender-authzed-fgam-endpoint file:///etc/spicedb/fgam.yaml +``` Use the following command-line flags: -| Flag | Description | Default | -| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| --extender-authzed-fgam-endpoint | defines the external SpiceDB endpoint used to authorize operations for the authzed-fgam extender. If a file:// endpoint is provided, server is run embedded with static configuration | | -| --extender-authzed-fgam-preshared-key | defines the external SpiceDB preshared key used to authorize operations for the authzed-fgam extender. Ignored if endpoint is local (file://) | | -| --extender-enabled | must be set to `authzed-fgam` | | +| Flag | Description | Default | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `--extender-authzed-fgam-endpoint` | defines the external SpiceDB endpoint used to authorize operations for the authzed-fgam extender. If a file:// endpoint is provided, server is run embedded with static configuration | | +| `--extender-authzed-fgam-preshared-key` | defines the external SpiceDB preshared key used to authorize operations for the authzed-fgam extender. Ignored if endpoint is local (file://) | | +| `--extender-enabled` | must be set to `authzed-fgam` | | If you set `--extender-authzed-fgam-endpoint` to a file, it must be a YAML configuration file. diff --git a/app/authzed/concepts/workload-isolation/page.mdx b/app/authzed/concepts/workload-isolation/page.mdx index 718846a9..f5c1bc92 100644 --- a/app/authzed/concepts/workload-isolation/page.mdx +++ b/app/authzed/concepts/workload-isolation/page.mdx @@ -7,7 +7,7 @@ import YouTube from "@/components/youtube-wrapper"; # Workload Isolation -Workload Isolation is functionality exclusive to AuthZed products by which we give your SpiceDB deployments access to hardware that is only used by you, to guarantee performance and prevent "noisy neighbor" problems. +Workload Isolation gives your SpiceDB deployments access to hardware that is only used by you, to guarantee performance and prevent "noisy neighbor" problems. ## Control Plane diff --git a/app/globals.css b/app/globals.css index a130fd32..7b1236d1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -289,3 +289,150 @@ html.dark-mode { display: inline-flex; } } + +/* ── Tables ───────────────────────────────────────────────────────────────── + Nextra's default MDX table is a full grid: 1px borders on every cell, zebra + rows, 16px body text, generous padding. On reference tables (flag lists, + feature matrices) that reads as a spreadsheet and fights the page. This + restyles every markdown table to horizontal rules only, a quiet mono + header row, and slightly smaller type, in the same register as the + Feature Matrix and the web comparison tables. Unlayered so it wins over + Nextra's `x:` utilities without !important. */ + +.nextra-content table, +article table { + --tbl-rule: hsl(280 5% 84%); + --tbl-head-rule: hsl(280 6% 60%); + --tbl-hover: hsl(300 8% 96%); + --tbl-head: hsl(280 4% 44%); + font-size: 0.875rem; + line-height: 1.5; + border-collapse: collapse; +} +html.dark .nextra-content table, +html.dark article table { + --tbl-rule: hsl(279 9% 22%); + --tbl-head-rule: hsl(279 8% 40%); + --tbl-hover: hsl(279 12% 11%); + --tbl-head: hsl(280 4% 58%); +} + +.nextra-content table tr, +article table tr { + border: 0; + border-bottom: 1px solid var(--tbl-rule); + background: transparent; +} +.nextra-content table thead tr, +article table thead tr { + border-bottom: 1px solid var(--tbl-head-rule); +} +.nextra-content table tbody tr:last-child, +article table tbody tr:last-child { + border-bottom: 1px solid var(--tbl-head-rule); +} +.nextra-content table tbody tr:hover, +article table tbody tr:hover { + background: var(--tbl-hover); +} + +.nextra-content table th, +.nextra-content table td, +article table th, +article table td { + border: 0; + padding: 0.55rem 0.75rem; + vertical-align: top; + text-align: left; +} +.nextra-content table th:first-child, +.nextra-content table td:first-child, +article table th:first-child, +article table td:first-child { + padding-left: 0.25rem; +} +.nextra-content table th:last-child, +.nextra-content table td:last-child, +article table th:last-child, +article table td:last-child { + padding-right: 0.25rem; +} + +/* Header row: mono, small, tracked, muted. A label, not a bold sentence. */ +.nextra-content table th, +article table th { + padding-top: 0.25rem; + padding-bottom: 0.5rem; + font-family: var(--font-mono, ui-monospace, monospace); + font-size: 0.7rem; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--tbl-head); +} + +/* Centered columns (Feature Matrix) keep their alignment from the markdown. */ +.nextra-content table th[style*="text-align: center"], +.nextra-content table td[style*="text-align: center"], +article table th[style*="text-align: center"], +article table td[style*="text-align: center"] { + text-align: center; +} + +/* First column is the row's identifier: keep it from collapsing, break it only + at a hyphen or space. Long code values (connection URIs) may break anywhere + rather than push the table sideways. */ +.nextra-content table td:first-child, +article table td:first-child { + min-width: 11rem; + overflow-wrap: normal; +} +.nextra-content table td code, +.nextra-content table td code.nextra-code, +article table td code, +article table td code.nextra-code { + font-size: 0.8em; + white-space: normal; + /* word-break, not overflow-wrap: Nextra's own `code.nextra-code` rule pins + overflow-wrap to break-word, which does not shrink a cell's min-content, + so a long URI would still push the table sideways. */ + word-break: break-word; +} +/* Trailing column is usually a short value (default, type). A floor keeps + `1000000` on one line while a long URI in the same slot still wraps. */ +.nextra-content table td:last-child, +article table td:last-child { + min-width: 6rem; +} + +/* ── Callouts ─────────────────────────────────────────────────────────────── + Nextra colours its with stock Tailwind hues (yellow-700 text on + yellow-50, blue-700 on blue-100, …), none of which are Sandworm. Re-key each + type to the site's own tokens, in the same tint/border/ink recipe the + product and feature pills use: 10% tint, 50% border, full-strength ink. + Types are matched on Nextra's own background class since the component + exposes no type attribute. */ + +.nextra-callout { + --co: var(--blue-500); + background: hsl(var(--co) / 0.1); + border-color: hsl(var(--co) / 0.5); + color: hsl(var(--co)); +} +.nextra-callout[class*="x:bg-yellow-"] { + --co: var(--sand-300); +} +.nextra-callout[class*="x:bg-red-"] { + --co: var(--red-400); +} +.nextra-callout[class*="x:bg-green-"] { + --co: var(--teal-500); +} +.nextra-callout[class*="x:bg-purple-"] { + --co: var(--violet-500); +} +/* Light mode: the tokens are mid-lightness, so ink needs to be darker than + the border to keep body text readable on the tint. */ +html:not(.dark) .nextra-callout { + color: hsl(from hsl(var(--co)) h s calc(l - 10)); +} diff --git a/app/materialize/api/client-sdks/page.mdx b/app/materialize/api/client-sdks/page.mdx index 5f591be5..0af9a011 100644 --- a/app/materialize/api/client-sdks/page.mdx +++ b/app/materialize/api/client-sdks/page.mdx @@ -5,8 +5,6 @@ description: "SDK versions with generated gRPC and protobuf support for the Auth # Client SDKs - - All SpiceDB SDKs have the generated gRPC and protobuf code - [authzed-go v0.15.0](https://github.com/authzed/authzed-go/releases/tag/v0.15.0) diff --git a/app/materialize/api/download-permission-sets/page.mdx b/app/materialize/api/download-permission-sets/page.mdx index 212ef933..9a448dbb 100644 --- a/app/materialize/api/download-permission-sets/page.mdx +++ b/app/materialize/api/download-permission-sets/page.mdx @@ -7,8 +7,6 @@ import { Callout } from "nextra/components"; # DownloadPermissionSets - - For large datasets, `DownloadPermissionSets` is a faster alternative to [LookupPermissionSets] for the initial [backfill](../concepts/permission-set-lifecycle): instead of streaming individual events over a single connection, it hands you a manifest of files you can fetch directly from blob storage, in parallel. ## Request diff --git a/app/materialize/api/lookup-permission-sets/page.mdx b/app/materialize/api/lookup-permission-sets/page.mdx index aab23e5c..59d4506b 100644 --- a/app/materialize/api/lookup-permission-sets/page.mdx +++ b/app/materialize/api/lookup-permission-sets/page.mdx @@ -7,8 +7,6 @@ import { Callout } from "nextra/components"; # LookupPermissionSets - - This API complements [WatchPermissionSets]. When you first bring on a system that needs permissions data, [LookupPermissionSets] lets you create an initial snapshot of the permissions data, and then you can use the [WatchPermissionSets] API to keep the snapshot updated. diff --git a/app/materialize/api/watch-permission-sets/page.mdx b/app/materialize/api/watch-permission-sets/page.mdx index 3542fa39..e8cd739e 100644 --- a/app/materialize/api/watch-permission-sets/page.mdx +++ b/app/materialize/api/watch-permission-sets/page.mdx @@ -7,8 +7,6 @@ import { Callout } from "nextra/components"; # WatchPermissionSets - - This is an update stream of all the permissions Materialize is configured to watch. You can use this to store all permissions tracked in the system closer to your application database to be used in database-native ACL filtering. Permissions can also be stored in secondary indexes like Elasticsearch. diff --git a/app/materialize/concepts/hydration/page.mdx b/app/materialize/concepts/hydration/page.mdx index c8407158..8d2fe1f9 100644 --- a/app/materialize/concepts/hydration/page.mdx +++ b/app/materialize/concepts/hydration/page.mdx @@ -5,8 +5,6 @@ description: "Hydration is how Materialize turns your SpiceDB schema and relatio # Hydration - - **Hydration** is the process Materialize uses to turn your SpiceDB schema and relationship data into the precomputed [permission sets](./permission-sets) that both Materialize features depend on: [Accelerated Queries](../getting-started/overview#accelerated-queries) reads hydrated data directly to answer checks and lookups, and [Event Streams](../getting-started/overview#event-streams) exposes that same hydrated data through `LookupPermissionSets` and `WatchPermissionSets`. ## Only what's reachable diff --git a/app/materialize/concepts/managing-client-state/page.mdx b/app/materialize/concepts/managing-client-state/page.mdx index 41a7d41f..0f6764c2 100644 --- a/app/materialize/concepts/managing-client-state/page.mdx +++ b/app/materialize/concepts/managing-client-state/page.mdx @@ -5,8 +5,6 @@ description: "State diagram of the transitions a client application moves throug # Managing Client State - - This diagram shows the various states your client application will need to transition through when calling the [LookupPermissionSets] and the [WatchPermissionSets] APIs. ![authzed-materialize](/images/materialize-client-state-diagram.png) diff --git a/app/materialize/concepts/permission-set-lifecycle/page.mdx b/app/materialize/concepts/permission-set-lifecycle/page.mdx index 34f4c4cc..f297d804 100644 --- a/app/materialize/concepts/permission-set-lifecycle/page.mdx +++ b/app/materialize/concepts/permission-set-lifecycle/page.mdx @@ -7,8 +7,6 @@ import { Callout } from "nextra/components"; # The permission set lifecycle - - The **permission set lifecycle** describes how your application's local copy of permission data is populated, kept current, and rebuilt over time — from an empty index to a complete, queryable [Permission Sets](./permission-sets) store, and back to a fresh index when your schema changes. When you first bring a system online, you have no permission data locally. diff --git a/app/materialize/concepts/permission-sets/page.mdx b/app/materialize/concepts/permission-sets/page.mdx index 211a59c4..227c6a95 100644 --- a/app/materialize/concepts/permission-sets/page.mdx +++ b/app/materialize/concepts/permission-sets/page.mdx @@ -7,8 +7,6 @@ import { Callout } from "nextra/components"; # Permission Sets - - A **permission set** is the unit of precomputed authorization data that Materialize produces. Where SpiceDB answers a permission question on demand by walking the relationship graph, Materialize continuously [**hydrates**](./hydration) the membership of the [permissions you configure it to watch](./watched-permissions) and exposes that denormalized data to your application. diff --git a/app/materialize/concepts/snapshots/page.mdx b/app/materialize/concepts/snapshots/page.mdx index fba99a11..262115ce 100644 --- a/app/materialize/concepts/snapshots/page.mdx +++ b/app/materialize/concepts/snapshots/page.mdx @@ -7,7 +7,6 @@ import { Callout } from "nextra/components"; # Snapshots - A **permission set snapshot** is a point-in-time, internally consistent view of every [Permission Set](./permission-sets) Materialize is tracking, computed at a specific SpiceDB revision. Every piece of permission data Materialize hands you is anchored to the revision (`ZedToken`) it was computed at: that revision _is_ the permission set snapshot's identity. diff --git a/app/materialize/concepts/watched-permissions/page.mdx b/app/materialize/concepts/watched-permissions/page.mdx index 4221dff3..0b538184 100644 --- a/app/materialize/concepts/watched-permissions/page.mdx +++ b/app/materialize/concepts/watched-permissions/page.mdx @@ -7,8 +7,6 @@ import { Callout } from "nextra/components"; # Watched Permissions - - A **watched permission** is the unit of configuration for Materialize: a `resource#permission@subject` tuple that tells Materialize which permission, for which subject type, to precompute. Everything Materialize does starts from the list of watched permissions you configure. diff --git a/app/materialize/getting-started/limitations/page.mdx b/app/materialize/getting-started/limitations/page.mdx index f1d60df4..375eb20a 100644 --- a/app/materialize/getting-started/limitations/page.mdx +++ b/app/materialize/getting-started/limitations/page.mdx @@ -5,8 +5,6 @@ description: "Current limitations of AuthZed Materialize, including caveats, wil # Limitations - - - Your schema can contain any of the following, but they cannot be on the path of your configured Materialize permissions or it will throw an error: - [Caveats] - [Wildcard] subject types diff --git a/app/materialize/guides/recommended-architecture/page.mdx b/app/materialize/guides/recommended-architecture/page.mdx index c43fb54f..f669e6fb 100644 --- a/app/materialize/guides/recommended-architecture/page.mdx +++ b/app/materialize/guides/recommended-architecture/page.mdx @@ -7,8 +7,6 @@ import { Callout } from "nextra/components"; # Recommended Architecture - - ## Event Processor ![authzed-materialize](/images/authzed-materialize.png) diff --git a/app/materialize/guides/relational-database/page.mdx b/app/materialize/guides/relational-database/page.mdx index 45e92372..4f3c8708 100644 --- a/app/materialize/guides/relational-database/page.mdx +++ b/app/materialize/guides/relational-database/page.mdx @@ -5,8 +5,6 @@ description: "Store Materialize permission sets in member_to_set and set_to_set # Syncing to a Relational Database - - Materialize is configured with a list of [watched permissions](../concepts/watched-permissions) — the `resource#permission@subject` tuples it pre-computes. This guide assumes you've already configured watched permissions like: ```zed diff --git a/app/review/page.tsx b/app/review/page.tsx new file mode 100644 index 00000000..49c6c1e5 --- /dev/null +++ b/app/review/page.tsx @@ -0,0 +1,158 @@ +import fs from "node:fs"; +import path from "node:path"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { Yes, No } from "@/components/feature-icon"; +import { PRODUCTS, PRODUCT_ORDER, availabilityForPath } from "@/lib/products"; +import "./review.css"; + +export const metadata = { title: "Product availability review" }; + +/* REVIEW AID — one table of every docs page and the product pill it renders, + so product can confirm the map in lib/products.ts in one sitting. Preview + deploys and local dev only, hidden from the nav. Delete this route once + the map is signed off. */ +const SHOW = process.env.NODE_ENV !== "production" || process.env.VERCEL_ENV === "preview"; + +/* Rows whose tiers come straight from a Feature Matrix row on + authzed/guides/picking-a-product. */ +const MATRIX = new Set([ + "/authzed/concepts/audit-logging", + "/authzed/concepts/restricted-api-access", + "/authzed/concepts/workload-isolation", + "/authzed/concepts/private-networking", + "/authzed/concepts/management-dashboard", + "/authzed/concepts/multi-region", + "/authzed/concepts/update-channels", + "/authzed/concepts/rate-limiting", + "/authzed/concepts/security-embargo", + "/authzed/guides/setting-up-private-networking", +]); + +/* Rows set by judgment with no matrix row or section rule to lean on. */ +const CONFIRM: Record = { + "/authzed/concepts/deployments": "Assumed Cloud + Dedicated (managed deployments).", + "/authzed/concepts/feature-maturity": "Assumed all managed tiers.", + "/authzed/api/http-api": "Assumed all managed tiers; page duplicates the SpiceDB HTTP API doc.", + "/mcp": "Assumed all four; unclear which products the MCP servers target.", + "/mcp/authzed/authzed-mcp-server": "Assumed all four.", + "/mcp/authzed/spicedb-dev-mcp-server": "Assumed all four; reads as an OSS dev tool.", + "/spicedb/getting-started/install": + "Install pages read “Not in Cloud, Dedicated”. True, but product may prefer no pill here.", +}; + +type Row = { route: string; title: string }; + +function collectPages(): Row[] { + const root = path.join(process.cwd(), "app"); + const out: Row[] = []; + const walk = (dir: string) => { + for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, ent.name); + if (ent.isDirectory()) walk(full); + else if (ent.name === "page.mdx") { + const rel = path.relative(root, path.dirname(full)); + const route = "/" + rel.split(path.sep).join("/"); + const title = + fs + .readFileSync(full, "utf8") + .match(/^title:\s*"?([^"\n]*)"?/m)?.[1] + ?.trim() ?? ""; + out.push({ route: route === "/." ? "/" : route, title }); + } + } + }; + walk(root); + return out.sort((a, b) => a.route.localeCompare(b.route)); +} + +function sourceOf(route: string): { kind: "matrix" | "section" | "confirm"; note?: string } { + if (MATRIX.has(route)) return { kind: "matrix" }; + for (const [prefix, note] of Object.entries(CONFIRM)) { + if (route === prefix || route.startsWith(prefix + "/")) return { kind: "confirm", note }; + } + return { kind: "section" }; +} + +const SOURCE_LABEL = { + matrix: "Feature Matrix", + section: "Section default", + confirm: "Please confirm", +} as const; + +export default function ReviewPage() { + if (!SHOW) notFound(); + const rows = collectPages().map((r) => ({ + ...r, + a: availabilityForPath(r.route), + src: sourceOf(r.route), + })); + const needs = rows.filter((r) => r.src.kind === "confirm").length; + + return ( +
+

Product availability review

+

+ Every docs page and the Available on pill it renders, from one map in{" "} + lib/products.ts. Click a page to see the pill in place. Rows marked{" "} + Please confirm ({needs}) were set by judgment; the rest come from a Feature + Matrix row or from the section the page lives in. To change a row, comment on the PR with + the page and the tiers. +

+
+ + available + + + DIY build it yourself + + + not in + + + · no pill on this page + +
+ + + + + {PRODUCT_ORDER.map((k) => ( + + ))} + + + + + {rows.map(({ route, title, a, src }) => ( + + + {PRODUCT_ORDER.map((k) => ( + + ))} + + + ))} + +
Page + {PRODUCTS[k].label} + Source
+ {title || route} +
{route}
+
+ {!a ? ( + · + ) : a.available.includes(k) ? ( + + ) : (a.diy ?? []).includes(k) ? ( + DIY + ) : ( + + )} + + {SOURCE_LABEL[src.kind]} + {src.note &&
{src.note}
} +
+
+ ); +} diff --git a/app/review/review.css b/app/review/review.css new file mode 100644 index 00000000..c1a75bee --- /dev/null +++ b/app/review/review.css @@ -0,0 +1,134 @@ +/* REVIEW AID — styles for app/review/page.tsx. Delete with the route. */ +.pa-review { + max-width: 1040px; + margin: 0 auto; + padding: 2rem 1rem 4rem; + font-size: 0.875rem; +} +.pa-review h1 { + font-size: 1.75rem; + font-weight: 700; + margin-bottom: 0.75rem; +} +.pa-review p { + max-width: 68ch; + line-height: 1.5; + margin-bottom: 1rem; +} +.pa-legend { + display: flex; + flex-wrap: wrap; + gap: 1.25rem; + margin: 0 0 1.5rem; + color: hsl(30 5% 50%); +} +.pa-legend > span { + display: inline-flex; + align-items: center; + gap: 0.4rem; +} +.pa-review table { + width: 100%; + border-collapse: collapse; +} +.pa-review thead th { + /* Sticky under the navbar so the tier columns stay labelled through 93 + rows. The banner is static and scrolls away, so it doesn't add to the + offset. Opaque ground from Nextra's own page colour so rows scroll + underneath cleanly. */ + position: sticky; + top: var(--nextra-navbar-height, 64px); + z-index: 1; + background: rgb(var(--nextra-bg)); +} +.pa-review th { + padding: 0.25rem 0.5rem 0.5rem; + border-bottom: 1px solid hsl(280 6% 60%); + font-family: var(--font-mono, ui-monospace, monospace); + font-size: 0.7rem; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: hsl(280 4% 44%); + text-align: left; +} +html.dark .pa-review th { + border-bottom-color: hsl(279 8% 40%); + color: hsl(280 4% 58%); +} +.pa-review td { + padding: 0.55rem 0.5rem; + border-bottom: 1px solid hsl(280 5% 84%); + vertical-align: top; +} +html.dark .pa-review td { + border-bottom-color: hsl(279 9% 22%); +} +.pa-center { + text-align: center !important; + width: 6.5rem; +} +.pa-route { + font-family: var(--font-mono, ui-monospace, monospace); + font-size: 0.7rem; + color: hsl(30 5% 50%); + margin-top: 0.15rem; +} +.pa-diy { + font-family: var(--font-mono, ui-monospace, monospace); + font-size: 0.65rem; + letter-spacing: 0.1em; + padding: 0.1rem 0.45rem; + border: 1px dashed hsl(30 5% 50% / 0.6); + border-radius: 9999px; +} +.pa-none { + color: hsl(30 5% 50% / 0.5); +} +.pa-src { + display: inline-block; + padding: 0.15rem 0.6rem; + border: 1px solid; + border-radius: 9999px; + font-size: 0.72rem; + font-weight: 500; + white-space: nowrap; +} +.pa-src-matrix { + color: hsl(var(--teal-500)); + border-color: hsl(var(--teal-500) / 0.5); + background: hsl(var(--teal-500) / 0.1); +} +.pa-src-section { + color: hsl(30 5% 50%); + border-color: hsl(30 5% 50% / 0.35); +} +.pa-src-confirm { + color: hsl(var(--sand-300)); + border-color: hsl(var(--sand-300) / 0.5); + background: hsl(var(--sand-300) / 0.1); +} +.pa-row-confirm td { + background: hsl(var(--sand-300) / 0.04); +} +.pa-note { + margin-top: 0.3rem; + max-width: 34ch; + font-size: 0.78rem; + line-height: 1.4; + color: hsl(30 5% 50%); +} + +/* Page links: this is a page.tsx, not MDX, so Nextra's prose link colour + doesn't reach here. Body colour with a sand underline, sand on hover. */ +.pa-review td a { + color: inherit; + font-weight: 500; + text-decoration: underline; + text-decoration-color: hsl(var(--sand-300) / 0.6); + text-underline-offset: 0.2em; +} +.pa-review td a:hover { + color: hsl(var(--sand-300)); + text-decoration-color: hsl(var(--sand-300)); +} diff --git a/components/content-status.css b/components/content-status.css index 837c7312..d100ab3f 100644 --- a/components/content-status.css +++ b/components/content-status.css @@ -28,19 +28,20 @@ opacity: 0.85; color: inherit; } +/* Sandworm tokens, pill recipe (10% tint, 50% border, full ink). Light mode + darkens the ink one step; the plain token is the fallback where relative + colour syntax is unsupported. Teal = new page, sand = updated. */ .content-status.is-new { - border-color: hsl(146 45% 38% / 0.5); - background: hsl(146 45% 45% / 0.12); - color: hsl(146 55% 26%); + --cs: var(--teal-500); } .content-status.is-updated { - border-color: hsl(38 72% 45% / 0.5); - background: hsl(38 72% 50% / 0.12); - color: hsl(32 70% 30%); + --cs: var(--sand-300); } -.dark .content-status.is-new { - color: hsl(146 50% 78%); +.content-status { + border-color: hsl(var(--cs) / 0.5); + background: hsl(var(--cs) / 0.1); + color: hsl(var(--cs)); } -.dark .content-status.is-updated { - color: hsl(40 85% 78%); +html:not(.dark) .content-status { + color: hsl(from hsl(var(--cs)) h s calc(l - 10)); } diff --git a/components/feature-icon.css b/components/feature-icon.css index 6cea4f29..1d386037 100644 --- a/components/feature-icon.css +++ b/components/feature-icon.css @@ -1,7 +1,8 @@ /* Feature-matrix marks. Theme-aware so the disc pops on either ground: - LIGHT (base) — a deeper, saturated teal→violet disc with a WHITE check. - DARK (html.dark) — the brighter Sandworm gradient with a dark check. - (The bright gradient alone washed out against white table cells.) */ + LIGHT (base) — a deep teal disc with a WHITE check. + DARK (html.dark) — a brighter teal disc with a dark check. + Teal only, from the shared --teal-* tokens: the earlier teal→violet + gradient read as light blue next to the sand pills. */ .feature-mark { display: inline-flex; @@ -12,17 +13,22 @@ border-radius: 9999px; vertical-align: middle; } +/* Glyph fills the disc: 15px in a 24px circle, strokeWidth 3 in the SVG. */ +.feature-mark svg { + width: 15px; + height: 15px; +} /* Yes — light (base) */ .feature-yes { - background: linear-gradient(135deg, hsl(177 33% 45%), hsl(178 35% 38%) 45%, hsl(253 60% 55%)); + background: linear-gradient(135deg, hsl(var(--teal-500)), hsl(var(--teal-700))); } .feature-yes svg { stroke: #fff; } /* Yes — dark */ html.dark .feature-yes { - background: linear-gradient(135deg, hsl(175 28% 73%), hsl(176 29% 57%) 45%, hsl(253 73% 63%)); + background: linear-gradient(135deg, hsl(var(--teal-400)), hsl(var(--teal-700))); } html.dark .feature-yes svg { stroke: hsl(280 37% 8%); /* stone-950 */ diff --git a/components/product-badge.css b/components/product-badge.css new file mode 100644 index 00000000..0d7074a8 --- /dev/null +++ b/components/product-badge.css @@ -0,0 +1,100 @@ +/* Product-availability pills — which AuthZed tiers a feature ships on. + Geometry and type match feature-badge.css so the two pill families read as + one system when they sit under adjacent H1s. Three states, three treatments: + sand = ships with it, dashed stone = you can build it yourself (the Feature + Matrix's DIY), solid stone = not available. The negative is a spelled-out "Not in …" pill, + never a struck or dimmed tier: absence has to read in one glance. */ + +.product-badge-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + margin: 1.5rem 0 1.75rem; +} + +.product-badge-label { + margin-right: 0.25rem; + font-family: var(--font-mono, ui-monospace, monospace); + font-size: 0.7rem; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: hsl(30 5% 50%); +} + +.product-badge { + display: inline-flex; + align-items: baseline; + gap: 0.4em; + padding: 0.25rem 0.75rem; + border: 1px solid; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; + text-decoration: none; +} + +/* --sand-300 flips itself under html.dark (app/globals.css). */ +.product-badge-yes { + color: hsl(var(--sand-300)); + background: hsl(var(--sand-300) / 0.1); + border-color: hsl(var(--sand-300) / 0.5); +} + +.product-badge-diy { + color: inherit; + background: transparent; + border-color: hsl(30 5% 50% / 0.6); + border-style: dashed; +} + +.product-badge-not { + color: inherit; + background: hsl(30 5% 50% / 0.12); + border-color: hsl(30 5% 50% / 0.45); +} + +.product-badge-suffix { + font-family: var(--font-mono, ui-monospace, monospace); + font-size: 0.6rem; + letter-spacing: 0.1em; + opacity: 0.85; +} + +.product-badge-glyph { + font-size: 0.7rem; + line-height: 1; +} + +.product-badge-sr { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + +/* Materialize: the feature pills join this row after a thin divider. The + feature row's own box dissolves so its pills become siblings here; the + inline early-access note (shown only below the TOC breakpoint) takes a + full line under the pills. */ +.product-badge-divider { + width: 1px; + height: 1.1rem; + margin: 0 0.25rem; + background: hsl(30 5% 50% / 0.35); +} +.product-badge-row .feature-badge-row { + display: contents; +} +.product-badge-row .feature-notes-inline { + flex-basis: 100%; + /* Its own margins assume it follows a row with a 1.75rem bottom margin + (negative top to pull up, 1.75rem bottom to space the body). Inside + this row the flex gap already separates it from the pills and the row + carries the bottom margin, so both go. */ + margin: 0.25rem 0 0; +} diff --git a/components/product-badge.tsx b/components/product-badge.tsx new file mode 100644 index 00000000..c421360c --- /dev/null +++ b/components/product-badge.tsx @@ -0,0 +1,80 @@ +"use client"; + +import "./product-badge.css"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { PRODUCTS, PRODUCT_ORDER, availabilityForPath, type ProductKey } from "@/lib/products"; +import { featuresForPath } from "@/lib/materialize-features"; +import { FeatureBadge } from "./feature-badge"; + +/* Which AuthZed products a page applies to. Rendered under every H1 by + mdx-components.ts; the tiers come from the route via lib/products.ts + (section defaults plus per-page facts), so pages carry no markup. Props + are accepted for a one-off placement with explicit tiers: + + + + Reads left to right as a sentence: AVAILABLE ON, the tiers that have it + (sand), the tiers where you build it yourself (dashed, DIY suffix; mirrors + the Feature Matrix's DIY cells), then one stone "Not in …" pill naming the + tiers that don't. The negative is spelled out rather than shown by + striking or dimming a pill: a reader who lands mid-page from a deep link + needs "not in Open Source" in one glance, not a decode. A page where + nothing is excluded gets no negative pill at all, and a page on exactly + one tier says "Dedicated only" instead of listing the three it is not on. + + On Materialize pages the feature pills (feature-badge.tsx) join this row + after a divider, so a page has one header strip, not two stacked rows. */ +type Props = { + available?: readonly ProductKey[]; + diy?: readonly ProductKey[]; +}; + +export function ProductBadge(props: Props) { + const pathname = usePathname(); + const fromRoute = availabilityForPath(pathname); + const hasFeatures = featuresForPath(pathname).length > 0; + const available = props.available ?? fromRoute?.available; + const diy = props.diy ?? fromRoute?.diy ?? []; + if (!available) return null; + + const yes = PRODUCT_ORDER.filter((k) => available.includes(k)); + const build = PRODUCT_ORDER.filter((k) => !available.includes(k) && diy.includes(k)); + const missing = PRODUCT_ORDER.filter((k) => !available.includes(k) && !diy.includes(k)); + const only = yes.length === 1 && build.length === 0 && missing.length > 0; + + return ( + + Available on + {yes.map((k) => ( + + {PRODUCTS[k].label} + {only && " only"} + + ))} + {build.map((k) => ( + + {PRODUCTS[k].label} + + (build it yourself) + + ))} + {missing.length > 0 && !only && ( + + + Not in {missing.map((k) => PRODUCTS[k].label).join(", ")} + + )} + {hasFeatures && ( + <> + + ); +} diff --git a/lib/products.ts b/lib/products.ts new file mode 100644 index 00000000..20de68af --- /dev/null +++ b/lib/products.ts @@ -0,0 +1,109 @@ +/* The AuthZed product tiers, in the order the Feature Matrix on + authzed/guides/picking-a-product lists them, plus which tiers each docs + page applies to. Single source for the product-availability pill + (components/product-badge.tsx), which mdx-components.ts renders under + every H1, so page markers and the matrix name the same tiers with the + same links and no page has to carry its own markup. + + Paths are basePath-relative: usePathname() strips NEXT_PUBLIC_BASE_DIR. */ + +export const PRODUCTS = { + "open-source": { + label: "Open Source", + href: "/authzed/guides/picking-a-product#open-source", + }, + cloud: { + label: "Cloud", + href: "/authzed/guides/picking-a-product#cloud", + }, + dedicated: { + label: "Dedicated", + href: "/authzed/guides/picking-a-product#dedicated", + }, + enterprise: { + label: "Enterprise", + href: "/authzed/guides/picking-a-product#enterprise", + }, +} as const; + +export type ProductKey = keyof typeof PRODUCTS; + +export const PRODUCT_ORDER: readonly ProductKey[] = [ + "open-source", + "cloud", + "dedicated", + "enterprise", +]; + +export type Availability = { + available: readonly ProductKey[]; + /* Tiers where you can build the equivalent yourself. Mirrors the Feature + Matrix's DIY cells. */ + diy?: readonly ProductKey[]; +}; + +const ALL: Availability = { available: PRODUCT_ORDER }; +const MANAGED: Availability = { available: ["cloud", "dedicated", "enterprise"] }; +const SELF_HOSTED: Availability = { available: ["open-source", "enterprise"] }; +const DEDICATED: Availability = { available: ["dedicated"] }; + +/* Section defaults, longest prefix wins. A page not matched by anything + here or in PAGE_AVAILABILITY renders no pill (the landing pages, the + product-comparison guide). */ +const SECTION_DEFAULTS: Record = { + "/spicedb": ALL, + /* Installing and operating the binary yourself: Cloud and Dedicated run it + for you, so these pages don't apply there. */ + "/spicedb/getting-started/install": SELF_HOSTED, + "/spicedb/ops/operator": SELF_HOSTED, + "/spicedb/ops/deploying-spicedb-operator": SELF_HOSTED, + "/spicedb/ops/eks": SELF_HOSTED, + "/authzed": MANAGED, + "/materialize": DEDICATED, + "/mcp": ALL, +}; + +/* Per-page facts, copied from the Feature Matrix where it has a row. Keep + this list and the matrix in step. */ +const PAGE_AVAILABILITY: Record = { + "/authzed/concepts/audit-logging": MANAGED, + "/authzed/concepts/restricted-api-access": { ...MANAGED, diy: ["open-source"] }, + "/authzed/concepts/workload-isolation": { + available: ["cloud", "dedicated"], + diy: ["open-source", "enterprise"], + }, + "/authzed/concepts/private-networking": { ...DEDICATED, diy: ["open-source", "enterprise"] }, + "/authzed/concepts/management-dashboard": { available: ["cloud", "dedicated"] }, + "/authzed/concepts/multi-region": { ...DEDICATED, diy: ["open-source", "enterprise"] }, + "/authzed/concepts/update-channels": { + available: ["cloud", "dedicated"], + diy: ["open-source", "enterprise"], + }, + "/authzed/concepts/rate-limiting": MANAGED, + "/authzed/concepts/security-embargo": MANAGED, + "/authzed/concepts/deployments": { available: ["cloud", "dedicated"] }, + "/authzed/concepts/feature-maturity": MANAGED, + "/authzed/guides/cloud": { available: ["cloud"] }, + "/authzed/guides/postgres-fdw": { available: ["cloud", "dedicated"] }, + "/authzed/guides/setting-up-private-networking": DEDICATED, + "/authzed/guides/picking-a-product": null, + "/authzed/api/http-api": MANAGED, + "/spicedb/getting-started/discovering-spicedb": null, + "/spicedb/getting-started/installing-zed": ALL, +}; + +export function availabilityForPath(pathname: string | null): Availability | null { + if (!pathname) return null; + const clean = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname; + if (clean in PAGE_AVAILABILITY) return PAGE_AVAILABILITY[clean]; + let best: string | null = null; + for (const prefix of Object.keys(SECTION_DEFAULTS)) { + if ( + (clean === prefix || clean.startsWith(prefix + "/")) && + (!best || prefix.length > best.length) + ) { + best = prefix; + } + } + return best ? SECTION_DEFAULTS[best] : null; +} diff --git a/mdx-components.ts b/mdx-components.ts index 699f653a..56e337f7 100644 --- a/mdx-components.ts +++ b/mdx-components.ts @@ -1,14 +1,24 @@ import { useMDXComponents as getDocsMDXComponents } from "nextra-theme-docs"; -import type { Component } from "react"; +import { createElement, Fragment, type Component, type ComponentProps } from "react"; import { Yes, No } from "@/components/feature-icon"; import { FeatureBadge } from "@/components/feature-badge"; +import { ProductBadge } from "@/components/product-badge"; const docsComponents = getDocsMDXComponents(); +const DocsH1 = docsComponents.h1!; + +/* Every page title carries the product-availability pill (lib/products.ts + decides the tiers from the route; pages with no entry render nothing). */ +function H1WithProducts(props: ComponentProps) { + return createElement(Fragment, null, createElement(DocsH1, props), createElement(ProductBadge)); +} export const useMDXComponents = (components?: Component) => ({ ...docsComponents, + h1: H1WithProducts, Yes, No, FeatureBadge, + ProductBadge, ...components, });