Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ public/robots.txt
public/sitemap-0.xml
public/sitemap.xml
public/feed.json

# macOS
.DS_Store
48 changes: 48 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,54 @@

@custom-variant dark (&:is(html[class~="dark"] *));

/* ===========================================================================
Sandworm accent tokens — site-wide.
Raw HSL triplets (consumed as `hsl(var(--teal-500) / 0.5)`), so any
component can tint without redeclaring a color. LIGHT is the base; DARK
overrides under `html.dark` (next-themes, which also resolves "system").
The light shades are deliberately DARKER than Sandworm's native register
so they stay legible on a light ground — don't "correct" them upward
without re-checking contrast.
Previously scoped under `.docs-home` in components/docs-home.css, which
put them out of reach of everything else on the site. The `--stone-*` ramp
stays scoped there: it's inverted for that page and isn't general-purpose.
======================================================================== */
:root {
--magenta-300: 316 50% 45%;
--magenta-400: 316 48% 47%;
--magenta-500: 316 50% 44%;
--magenta-600: 316 52% 40%;
--magenta-950: 315 33.3% 7.1%;
--red-400: 352 70% 48%;
--teal-300: 178 40% 32%;
--teal-400: 178 38% 35%;
--teal-500: 178 35% 33%;
--teal-700: 178 30% 26%;
--violet-400: 253 60% 52%;
--violet-500: 253 62% 50%;
--violet-700: 255 50% 46%;
--sand-300: 30 75% 42%;
--blue-500: 200 75% 40%;
}

html.dark {
--magenta-300: 318 41.2% 70%;
--magenta-400: 317 40.2% 60%;
--magenta-500: 316 39.2% 51%;
--magenta-600: 316 42% 45%;
--magenta-950: 315 33.3% 7.1%;
--red-400: 355 91% 73.9%;
--teal-300: 175 27.5% 72.9%;
--teal-400: 176 28% 56%;
--teal-500: 177 28.2% 45.9%;
--teal-700: 178 30% 30%;
--violet-400: 253 76.5% 70%;
--violet-500: 253 73.4% 63.1%;
--violet-700: 255 58% 43.9%;
--sand-300: 28 100% 72%;
--blue-500: 195 73.4% 63.1%;
}

@theme {
--breakpoint-*: initial;
--breakpoint-sm: 640px;
Expand Down
2 changes: 2 additions & 0 deletions app/materialize/api/client-sdks/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ description: "SDK versions with generated gRPC and protobuf support for the Auth

# Client SDKs

<FeatureBadge />

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)
Expand Down
2 changes: 2 additions & 0 deletions app/materialize/api/lookup-permission-sets/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Callout } from "nextra/components";

# LookupPermissionSets

<FeatureBadge />

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.

Expand Down
2 changes: 2 additions & 0 deletions app/materialize/api/watch-permission-sets/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Callout } from "nextra/components";

# WatchPermissionSets

<FeatureBadge />

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.
Expand Down
4 changes: 3 additions & 1 deletion app/materialize/concepts/_meta.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export default {
"watched-permissions": "Watched Permissions (Needs Review)",
snapshots: "Snapshots",
hydration: "Hydration (Needs Review)",
"permission-sets": "Permission Sets",
"permission-set-lifecycle": "Permission Set Lifecycle",
snapshots: "Snapshots",
"managing-client-state": "Managing Client State",
};
26 changes: 26 additions & 0 deletions app/materialize/concepts/hydration/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: "Hydration"
description: "Hydration is how Materialize turns your SpiceDB schema and relationships into precomputed permission sets, computing only what's reachable from the permissions you've configured it to watch."
---

# Hydration

<FeatureBadge />

**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

Hydration doesn't compute every permission your schema could produce, only the [permissions you've configured Materialize to watch](./watched-permissions), plus everything those permissions structurally depend on through unions, intersections, exclusions, and arrows. This **reachability** is a pure function of your schema and your watched permissions, so Materialize computes it once per hydration run and reuses it throughout.

This is also why the cost of hydration tracks the size of the reachable graph, not the size of your whole schema: watching a permission with a much wider reach, or adding a new watched permission, can noticeably change how much work hydration has to do.

## Offline hydration vs. online hydration

- **Offline hydration** reads a full [snapshot](./snapshots) of your relationship data and computes every reachable permission set from scratch. This is what backs the initial [backfill](./permission-set-lifecycle).
- **Online hydration** applies as new relationship and schema changes arrive after that initial pass, updating only the permission sets a given change actually affects, in the order the changes occurred.

## When rehydration is required

A [breaking schema change](./snapshots#snapshot-lifecycle-events) invalidates previously hydrated permission sets, since the reachability computed from the old schema no longer holds.
Materialize signals this so you can re-run the [backfill](./permission-set-lifecycle) at the new revision. See [The Permission Set Lifecycle](./permission-set-lifecycle) for the full rebuild flow.
2 changes: 2 additions & 0 deletions app/materialize/concepts/managing-client-state/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ description: "State diagram of the transitions a client application moves throug

# Managing Client State

<FeatureBadge />

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)
Expand Down
2 changes: 2 additions & 0 deletions app/materialize/concepts/permission-set-lifecycle/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Callout } from "nextra/components";

# The permission set lifecycle

<FeatureBadge />

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.
Expand Down
4 changes: 3 additions & 1 deletion app/materialize/concepts/permission-sets/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { Callout } from "nextra/components";

# Permission Sets

<FeatureBadge />

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 **precomputes** the membership of the permissions you configure it to watch and exposes that denormalized data to your application.
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.

Conceptually, a permission set captures two kinds of edges:

Expand Down
5 changes: 4 additions & 1 deletion app/materialize/concepts/snapshots/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import { Callout } from "nextra/components";

# Snapshots

<FeatureBadge />

A **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 snapshot's identity.

Snapshots are what make the two Materialize APIs compose safely:
Snapshots underpin both Materialize features: they're what Materialize [hydrates](./hydration) in order to answer Accelerated Queries, and they're what make the Event Streams APIs compose safely:

- Materialize reads the current snapshot directly to answer Accelerated Queries like `CheckPermission`, `CheckBulkPermissions`, `LookupResources`, and `LookupSubjects`.
- [The initial backfill](./permission-set-lifecycle) reads a snapshot in full.
- [WatchPermissionSets](../api/watch-permission-sets) advances that snapshot forward, delivering the deltas that move it from one revision to the next.

Expand Down
53 changes: 53 additions & 0 deletions app/materialize/concepts/watched-permissions/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: "Watched Permissions"
description: "A watched permission is a resource#permission@subject tuple that tells Materialize which permission to precompute — the configuration that drives hydration and everything built on top of it."
---

import { Callout } from "nextra/components";

# Watched Permissions

<FeatureBadge />

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.

## Format

A watched permission is written the same way you'd write a relationship or a check, minus the specific object IDs:

```zed
resource#view@user
resource#edit@user
```

Each tuple names:

- The **resource type** holding the permission (`resource`)
- The **permission or relation** to watch (`view`, `edit`)
- The **subject type** Materialize should compute membership for (`user`)

If you only care about a specific relation on the subject (for example, `group#member` rather than the group itself) you can specify that too: `resource#view@group#member`.

## What watching a permission actually does

Materialize doesn't precompute your whole schema. It only tracks your watched permissions and whatever they structurally reach through unions, intersections, exclusions, and arrows. That reachable set is exactly what [hydration](./hydration) computes and keeps up to date.

Because reachability is a pure function of your schema and your watched permissions, adding a new watched permission, or watching a permission with a much wider reach through your schema, extends that graph, and with it, the amount of work hydration has to do.

## Constraints

A watched permission must reference real schema elements: an existing resource type, an existing permission or relation on it, and an existing subject type. Materialize validates this when you configure it and will reject a permission that doesn't resolve.

The permission's path also can't cross any of the schema features Materialize doesn't support. See [Limitations](../getting-started/limitations) for the full list.

## Configuring watched permissions

<Callout type="info">
During early access, work with your AuthZed account team to configure your list of watched
permissions.
</Callout>

[Caveats]: https://authzed.com/docs/spicedb/concepts/caveats
[Wildcard]: https://authzed.com/docs/spicedb/concepts/schema#wildcards
[.all intersections]: https://authzed.com/docs/spicedb/concepts/schema#all-intersection-arrow
2 changes: 1 addition & 1 deletion app/materialize/getting-started/_meta.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default {
overview: "Overview",
overview: "Overview (Needs Review)",
limitations: "Limitations",
};
35 changes: 28 additions & 7 deletions app/materialize/getting-started/overview/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,45 @@ description: "AuthZed Materialize precomputes the permissions you configure it t
---

import { Callout } from "nextra/components";
import MaterializeArchitecture from "@/public/images/materialize-architecture.svg";

# What is Materialize?

AuthZed Materialize takes inspiration from the Leopard index component described in the [Zanzibar paper](https://zanzibar.tech/2IoYDUFMAE:0:T).
Much like the concept of a materialized view in relational databases, AuthZed Materialize is a service that you configure with a list of permissions that you want it to precompute, and it will calculate how those permissions change after relationships
are written (specifically, when those relationships affect a subject's membership in a permission set or a set's permission on a specific resource), or when a new schema is written.

Materialize puts that precomputed data to work in two ways: **Accelerated Queries** and **Event Streams**, both currently in early access. Every other piece of Materialize content belongs to one of these two features. Look for badges at the top of the page, which link back to the feature it belongs to.

<MaterializeArchitecture
role="img"
aria-label="The services that make up AuthZed Materialize, and how they connect to your application"
className="w-full h-auto"
/>

## Accelerated Queries

<Callout type="info">
AuthZed Materialize is available to users of AuthZed [Dedicated] as part of an early access
Accelerated Queries is available to users of AuthZed [Dedicated] as part of an early access
program. Don't hesitate to get in touch with your AuthZed account team if you would like to
participate.
</Callout>

AuthZed Materialize takes inspiration from the Leopard index component described in the [Zanzibar paper](https://zanzibar.tech/2IoYDUFMAE:0:T).
Much like the concept of a materialized view in relational databases, AuthZed Materialize is a service that you configure with a list of permissions that you want it to precompute, and it will calculate how those permissions change after relationships
are written (specifically, when those relationships affect a subject's membership in a permission set or a set's permission on a specific resource), or when a new schema is written.
These precomputed permissions can then be used either to provide faster checks and lookups through Accelerated Queries, or streamed into your own application database and **colocated** alongside the data they protect — a pattern known as **colocated permissions** — so you can search, sort, filter, and paginate over authorized objects natively and much more efficiently.

In summary, AuthZed Materialize allows you to:
Accelerated Queries use Materialize's precomputed permissions cache to answer the SpiceDB queries your application already makes, without changing how you call SpiceDB:

- Speed up `CheckPermission` and `CheckBulkPermissions`.
- Speed up `LookupResources` and `LookupSubjects`, especially when there is a large number of resources.

## Event Streams

<Callout type="info">
Event Streams is available to users of AuthZed [Dedicated] as part of an early access program.
Don't hesitate to get in touch with your AuthZed account team if you would like to participate.
</Callout>

Event Streams let you take Materialize's precomputed permission data out of SpiceDB and keep your own copy of it — either **colocated** alongside the data it protects in your application database, or in a secondary index like a search engine, a pattern known as **colocated permissions**.

- Download the current state of every [permission set](../../concepts/permission-sets) with `LookupPermissionSets`, then keep it current in real time with `WatchPermissionSets`. See [The Permission Set Lifecycle](../../concepts/permission-set-lifecycle).
- Build authorization-aware UIs — sort, filter, and paginate over many thousands of authorized objects natively in your own database by colocating computed permissions next to your application data.
- Perform ACL filtering in secondary indexes too, like a search index (e.g. Elasticsearch) — authorization-aware UIs and search-index filtering are the same idea (ACL filtering) applied to different stores.

Expand Down
6 changes: 4 additions & 2 deletions app/materialize/guides/recommended-architecture/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { Callout } from "nextra/components";

# Recommended Architecture

## Consuming Client
<FeatureBadge />

## Event Processor

![authzed-materialize](/images/authzed-materialize.png)

Customers will need to build a client to act as an "event processor" that consumes permission updates and writes those updates to a datastore like Postgres.
Customers will need to build a client to act as an event processor that consumes permission updates and writes those updates to a datastore like Postgres.
The consumer should be designed with resumability in mind by keeping track of the last revision consumed, just as any other stream processor.

## Durability
Expand Down
13 changes: 3 additions & 10 deletions app/materialize/guides/relational-database/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,17 @@ title: "Syncing to a Relational Database"
description: "Store Materialize permission sets in member_to_set and set_to_set tables and use SQL joins for authorization-aware filtering, with worked examples."
---

import { Callout } from "nextra/components";

# Syncing to a Relational Database

Just as with relational database materialized views, you need to provide Materialize with the "queries" you'd like it to pre-compute.
The configuration is described as a list of `resource#permission@subject` tuples.
Example:
<FeatureBadge />

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
resource#view@user
resource#edit@user
```

<Callout type="info">
During early access provisioning, Materialize instances are not self-service, so you'll need to
provide the permissions to be computed by Materialize directly to your AuthZed account team.
</Callout>

## Relational Database

You can find a runnable version of these examples [here](https://dbfiddle.uk/dX10Cu3Z).
Expand Down
35 changes: 4 additions & 31 deletions components/docs-home.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,9 @@
--stone-900: 300 24% 94%;
--stone-950: 300 30% 96%;
--stone-975: 300 40% 98%;
/* ---- Accents — LIGHT (darker shades so they read on a light ground) ---- */
--magenta-300: 316 50% 45%;
--magenta-400: 316 48% 47%;
--magenta-500: 316 50% 44%;
--magenta-600: 316 52% 40%;
--magenta-950: 315 33.3% 7.1%;
--red-400: 352 70% 48%;
--teal-300: 178 40% 32%;
--teal-400: 178 38% 35%;
--teal-500: 178 35% 33%;
--teal-700: 178 30% 26%;
--violet-400: 253 60% 52%;
--violet-500: 253 62% 50%;
--violet-700: 255 50% 46%;
--sand-300: 30 75% 42%;
--blue-500: 200 75% 40%;
/* ---- Accents (magenta/red/teal/violet/sand/blue) now live site-wide on
`:root` + `html.dark` in app/globals.css, so components outside this page
can use them too. Both light and dark values moved together. ---- */
/* ---- Shared structural tokens ---- */
--gradient-brand-warm: linear-gradient(
to right,
Expand Down Expand Up @@ -81,21 +68,7 @@ html.dark .docs-home {
--stone-900: 278 28.6% 11%;
--stone-950: 280 36.6% 8%;
--stone-975: 282 50% 3.9%;
--magenta-300: 318 41.2% 70%;
--magenta-400: 317 40.2% 60%;
--magenta-500: 316 39.2% 51%;
--magenta-600: 316 42% 45%;
--magenta-950: 315 33.3% 7.1%;
--red-400: 355 91% 73.9%;
--teal-300: 175 27.5% 72.9%;
--teal-400: 176 28% 56%;
--teal-500: 177 28.2% 45.9%;
--teal-700: 178 30% 30%;
--violet-400: 253 76.5% 70%;
--violet-500: 253 73.4% 63.1%;
--violet-700: 255 58% 43.9%;
--sand-300: 28 100% 72%;
--blue-500: 195 73.4% 63.1%;
/* Dark accents also moved to app/globals.css (`html.dark`). */

background:
radial-gradient(120% 80% at 88% -10%, hsl(var(--magenta-950) / 0.55), transparent 60%),
Expand Down
Loading
Loading