Skip to content
Tutorials

Reference private platform records

Maya connects her account to a Calendar integration. She needs to choose the registered application, but she shouldn't be able to read its private platform settings.

A foreign key can express “this connection uses Calendar” without making Calendar's configuration readable. In a permission-enabled schema, mark that relationship with @rent.reference.

Keep the two kinds of data separate

This example extends an application with a tenant-scoped Principal model representing the signed-in person:

model Application {
  id               String @id @rent.manual
  private_settings Json
  @@rent.system
}

model Connection {
  tenant_id      String
  id             String @rent.manual
  owner_id       String
  application_id String

  owner Principal @relation(fields: [tenant_id, owner_id], references: [tenant_id, id]) @rent.creator
  application Application @relation(fields: [application_id], references: [id]) @rent.reference

  @@id([tenant_id, id])
}

permissions Connection {
  read, create, update, delete = owner
}

The platform manages Application records through trusted code. Maya owns her Connection. @rent.creator supplies its owner. @rent.reference allows the connection to refer to an existing Application without granting read access to that application's fields.

Create the connection as Maya

After the platform has registered Calendar, her scoped client uses an ordinary create:

let connection = maya.create_connection()
    .id("first")
    .application_id("calendar")
    .save()
    .await?;

Rent checks that Calendar exists and applies the Connection's owner and tenant rules. The handler doesn't need a system client just to make this reference.

Reference a record without exposing it

Maya still can't browse Application records or eagerly load Calendar's private settings through this connection. The annotation grants no ability to edit the application.

Using it does let an authorized connection writer discover whether the supplied application ID exists. Choose this pattern when that is appropriate for your product. Other product rules, such as whether an integration is enabled for an organization, belong in the application's command or an explicit policy.

For supported relationship shapes, see system references.

Complete example

Download this project, or copy the files below into an empty directory.

Replace RENT_REPOSITORY_URL in Cargo.toml with the URL supplied with your preview access. Use the matching Rent CLI release.

Use a separate local database for this example:

export DATABASE_URL='sqlite://lesson.db?mode=rwc'
rent generate
rent migrate dev --name initial
cargo run

Run the included test with cargo nextest run. It creates an isolated temporary database.

Cargo.toml
[package]
name = "rent-lesson-41-system-references"
version = "0.1.0"
edition = "2024"

[workspace]

[[bin]]
name = "rent-lesson-41-system-references"
path = "main.rs"

[dependencies]
rent = { git = "RENT_REPOSITORY_URL", tag = "v0.7.2", default-features = false, features = ["sqlite", "migrate", "extensions"] }
anyhow = "1.0.104"
tokio = { version = "1.53.1", features = ["full"] }
tempfile = "3.27.0"
rent.toml
schema = "schema.rsl"
generated = "generated"
migrations = "migrations"

[database]
dialect = "sqlite"
backends = ["sqlite"]
schema.rsl
datasource db {
  provider = "sqlite"
}

tenancy {
  scope = "required"
  field = "tenant_id"
}

access {
  subjects = [Principal]
}

model Principal {
  tenant_id String
  id        String

  @@id([tenant_id, id])
}

model Application {
  id               String  @id  @rent.manual
  private_settings Json

  @@rent.system
}

model Connection {
  tenant_id      String
  id             String       @rent.manual
  owner_id       String
  application_id String
  owner          Principal    @relation(fields: [tenant_id, owner_id], references: [tenant_id, id])  @rent.creator
  application    Application  @relation(fields: [application_id], references: [id])  @rent.reference

  @@id([tenant_id, id])
}

permissions Principal {
  read                   = anyone
  create, update, delete = nobody
}

permissions Connection {
  read, create, update, delete = owner
}
main.rs
use anyhow::{Result, ensure};
use rent::policy::viewer::Viewer;
#[allow(dead_code, unused_imports)]
#[rustfmt::skip]
#[path = "generated/mod.rs"]
mod generated;
use generated::client::Client;
use generated::principal::{Principal, PrincipalId};
async fn example(url: &str) -> Result<()> {
    let root = Client::connect(url).await?;
    let system = root.system();
    system
        .upsert_principal(generated::principal::unique::tenant_id_id)
        .tenant_id("acme")
        .id("maya")
        .save()
        .await?;
    system
        .upsert_application(generated::application::unique::id)
        .id("calendar")
        .private_settings(rent::serde_json::json!({"secret": "example-only"}))
        .save()
        .await?;
    let maya = root.with_viewer(
        Viewer::of(Principal::subject(PrincipalId {
            tenant_id: "acme".into(),
            id: "maya".into(),
        }))
        .tenant("acme"),
    );
    let connection = maya
        .create_connection()
        .id(rent::uuid::Uuid::now_v7().to_string())
        .application_id("calendar")
        .save()
        .await?;
    ensure!(connection.owner_id == "maya");
    ensure!(maya.application().all().await.is_err());
    Ok(())
}

#[tokio::main]
async fn main() -> Result<()> {
    example(&std::env::var("DATABASE_URL")?).await
}
#[cfg(test)]
mod test_support;
#[cfg(test)]
mod tests {
    #[tokio::test]
    async fn example_runs() -> anyhow::Result<()> {
        let (_directory, url) = super::test_support::database()?;
        super::example(&url).await
    }
}
test_support.rs
use anyhow::{Context, Result, ensure};
use std::process::Command;

pub fn database() -> Result<(tempfile::TempDir, String)> {
    let directory = tempfile::tempdir()?;
    std::fs::write(directory.path().join("schema.rsl"), include_str!("schema.rsl"))?;
    std::fs::write(directory.path().join("rent.toml"), r#"schema = "schema.rsl""#)?;
    let url = format!("sqlite://{}?mode=rwc", directory.path().join("app.db").display());
    let executable = std::env::var("RENT_CLI").unwrap_or_else(|_| "rent".into());
    let output = Command::new(executable)
        .arg("migrate")
        .arg("dev")
        .arg("--root")
        .arg(directory.path())
        .arg("--to")
        .arg("schema.rsl")
        .arg("--database-url")
        .arg(&url)
        .output()
        .context("run the Rent CLI")?;
    ensure!(
        output.status.success(),
        "migration failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    Ok((directory, url))
}