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 runRun 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))
}