{"record":{"id":"998dfdbb7e7ba65e","repo":"clockworklabs/SpacetimeDB","slug":"discovered-cyclic-dependency-when-resolving-rls-ru","errorCode":null,"errorMessage":"Discovered cyclic dependency when resolving RLS rules for table id `{table_id}`","messagePattern":"Discovered cyclic dependency when resolving RLS rules for table id `(.+?)`","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/expr/src/rls.rs","lineNumber":237,"sourceCode":"    let mut names = vec![];\n    view.visit(&mut |expr| match expr {\n        RelExpr::RelVar(rhs)\n        | RelExpr::LeftDeepJoin(LeftDeepJoin { rhs, .. })\n        | RelExpr::EqJoin(LeftDeepJoin { rhs, .. }, ..)\n            if !is_return_table(rhs) =>\n        {\n            names.push((rhs.schema.table_id, rhs.alias.clone()));\n        }\n        _ => {}\n    });\n\n    // Are we currently resolving any of them?\n    if let Some(table_id) = names\n        .iter()\n        .map(|(table_id, _)| table_id)\n        .find(|table_id| resolving.contains(table_id))\n    {\n        anyhow::bail!(\"Discovered cyclic dependency when resolving RLS rules for table id `{table_id}`\");\n    }\n\n    let return_name = |expr: &ProjectName| {\n        expr.return_name()\n            .map(|name| name.to_owned())\n            .ok_or_else(|| anyhow::anyhow!(\"Could not resolve table reference in RLS filter\"))\n    };\n\n    let mut view_def_fragments = vec![];\n\n    for (table_id, alias) in names {\n        let mut view_fragments = vec![];\n\n        for sql in tx.rls_rules_for_table(table_id)? {\n            // Parse and type check the RLS filter\n            let (expr, is_parameterized) = parse_and_type_sub(&sql, tx, auth)?;\n\n            // Are any of the RLS rules parameterized?","sourceCodeStart":219,"sourceCodeEnd":255,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/6dee26c6efc2856793e12b148a59742964f5d783/crates/expr/src/rls.rs#L219-L255","documentation":"RLS rules are SQL filters that may reference other tables. The resolver keeps a set of tables currently being resolved; if resolving a table's filter requires a table already on that stack (directly or transitively), a reference cycle exists and resolution aborts with this error.","triggerScenarios":"Two or more RLS rules that reference each other's tables: table A's filter joins B, and B's filter (directly or through a view chain) joins back to A.","commonSituations":"Mutual-authorization patterns ('row visible if a linked row in the other table exists') declared as RLS on both tables; RLS filters joining through views that loop back to the origin table.","solutions":["Break the cycle: remove one direction of reference and move the shared predicate into a third table that both filters reference without back-edges.","Denormalize the authorization fact into a single table so only one RLS rule needs the join.","Do the authorization check inside a reducer for one of the tables instead of declaring RLS on it."],"exampleFix":"-- before: mutual RLS cycle (a -> b, b -> a)\nCREATE TABLE a ... WITH ROW LEVEL SECURITY (SELECT ... FROM b WHERE b.owner = :sender);\nCREATE TABLE b ... WITH ROW LEVEL SECURITY (SELECT ... FROM a WHERE a.owner = :sender);\n\n-- after: single direction, or flatten ownership into one table\nCREATE TABLE a ... WITH ROW LEVEL SECURITY (owner = :sender);\nCREATE TABLE b ...; -- authorized via reducer writes","handlingStrategy":"validation","validationCode":"-- before enabling RLS, sketch the reference graph of all rules:\n-- rule on A references B?, rule on B references A?, views chain back?\n-- reject any rule whose referenced set reaches back to its own table\n-- (simple check while reviewing the module source)","typeGuard":null,"tryCatchPattern":"match publish(&db, &module).await {\n    Err(e) if e.to_string().contains(\"cyclic dependency\") => {\n        // remove one direction of the A<->B reference, republish\n    }\n    other => other,\n}","preventionTips":["Draw the RLS reference graph before adding a rule that joins another table.","Keep RLS filters referencing at most one level of helper table, never back-edges.","Centralize authorization facts in a single table instead of mutual rules."],"tags":["rls","row-level-security","cyclic-dependency","recursion","schema"],"backgroundTag":"circular-dependency","analyzedSha":"6dee26c6efc2856793e12b148a59742964f5d783","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}