SpiceDB Documentation
Concepts
Writing Relationships that Expire

Writing Relationships that Expire

A common use case is granting a user access to a resource for a limited time.

Before SpiceDB v1.40, caveats were the recommended way to support time-bound permissions, but that has some limitations:

  • It requires clients to provide the now timestamp. This is additional complexity for clients.
  • Expired caveats are not automatically garbage collected. This can lead to many caveated relationships in the system and increase the costs of loading and evaluating those into the runtime.

After SpiceDB v1.4.0, and if you need to grant temporary access to a resource, you can do so by writing relationships that expire after a certain time.

The time must be specified in RFC 3339 format (opens in a new tab).

The clock used to determine if a relationship is expired is that of the underlying SpiceDB datastore. This gets trickier when using distributed databases like CockroachDB or Spanner, where clocks have an uncertainty range. When operating your own database, it's key to keep node clocks in sync - we recommend services like Amazon Time Sync Service (opens in a new tab). You should evaluate the impact of clock drift in your application.

Schema

To enable expiration in your schema, add a use expiration clause to the top of the file. Then the relations subject to expiration are marked using <type> with expiration:

use expiration
 
definition user {}
 
definition resource {
    relation viewer: user with expiration
}

API

To write a relationship that expires, use the WriteRelationships or BulkImportRelationships APIs, and set the OptionalExpiresAt field in the relationship:

WriteRelationshipsRequest {
  Updates: [
    RelationshipUpdate{
      Operation: TOUCH
      Relationship: {
        Resource: {
            ObjectType: "resource",
            ObjectId: "someresource",
        },
        Relation: "viewer",
        Subject: {
            ObjectType: "user",
            ObjectId: "sarah",
        },
        OptionalExpiresAt: "2022-12-31T23:59:59Z"
      }
    }
  ]
}
⚠️

When using the WriteRelationships API, it is recommended to always use the TOUCH operation to create and update expiring relationships. If a relationship has expired but has not yet been garbage collected, using the CREATE operation will return an error for that relationship.

Playground

To write a relationship that expires, use the following format:

resource:someresource#viewer@user:anne[expiration:2025-12-31T23:59:59Z]

or specify expirations in the Expiration column in the Relationship grid editor.


zed

To write a relationship that expires, use the --expiration-time flag:

zed relationship create resource:someresource viewer user:anne --expiration-time "2025-12-31T23:59:59Z"

Garbage Collection

As soon as a relationship expires, it will no longer be used in permission checks. However, the relationship in the datastore is not deleted right then, but rather is subject to garbage collection.

Reclaiming expiring relationships is governed by the same mechanism (and flags) as the deletion of the history of relationship changes that powers SpiceDB's own MVCC (Multi-Version Concurrency Control) and heavily depends on the datastore chosen.

  • Datastores like Spanner and CockroachDB have built-in support for expiring SQL rows, so the database does Garbage Collection. In both cases, expired relationships will be reclaimed after 24 hours, which can't be changed without directly manipulating the SQL schema.
  • Datastores like Postgres and MySQL support it using the same GC job that reclaims old relationship versions, which runs every 5 minutes. Unlike Spanner and CockroachDB, you can govern the GC window with the corresponding flags. Relationships will be reclaimed after 24 hours by default.

The GC Window should be adjusted according to the application's needs. How far back in time does your application need to go? If this is a common use case, we recommend drastically reducing the GC window (e.g., 1 hour or 30 minutes). This means SpiceDB will have to evaluate less data when serving authorization checks, which can improve performance drastically in large-scale deployments.

Migrating Off Of Expiration With Caveats

If you implemented expiration using caveats, this section describes migrating to the new expiration feature.

  1. Rename your caveat if you had named it expiration

  2. Add the new subject type to your relation, and also add a combination where both are used. For example:

    caveat ttl(timeout duration, now string, timeout_creation_timestamp string) {
       timestamp(now) - timestamp(timeout_creation_timestamp) < timeout
    }
     
    definition user {}
     
    definition resource {
        relation viewer: user with ttl
    }

    Becomes:

    use expiration
     
    caveat ttl(timeout duration, now string, timeout_creation_timestamp string) {
       timestamp(now) - timestamp(timeout_creation_timestamp) < timeout
    }
     
    definition user {}
     
    definition resource {
        relation viewer: user with ttl | user with expiration | user with ttl and expiration
    }
  3. Migrate all relationships to use both the caveat and the new expiration. This is needed because only one relationship is allowed for a resource/permission/subject combination.

  4. Validate that the new expiration feature works as expected by not providing the context for evaluating the ttl caveat.

  5. Once validated, migrate completely to the new expiration feature by writing all relationships with only expiration and without caveat.

  6. Drop the caveat from your schema once the migration is completed:

    use expiration
     
    definition user {}
     
    definition resource {
        relation viewer: user with expiration
    }
© 2025 AuthZed.