Parts 1 and 2 of this series covered data access fundamentals and advanced business modeling. But enterprise deployments need more than access and intelligence -- they need governance. Who can see what data? How do agents handle multi-step analyses? How does the system resolve ambiguous business terms like "Nike" or "MSFT"? And what happens when the answer requires combining data from multiple databases? Part 3 covers the capabilities that make DAS enterprise-ready: row-level security, scratch pad working memory, named entity recognition, and cross-database federation.
Code examples are simplified for clarity. See the DAS documentation for complete API reference.
Row-Level Security: Agents See Only What They Should
When an AI agent queries your orders table, should it see all orders or only the ones belonging to its assigned customers? Row-level security (RLS) in DAS answers this question through security predicates -- AST expressions attached to stored view definitions that are transparently injected as WHERE clauses at query time.
How It Works
- Every DAS request carries an identity via the
X-On-Behalf-Ofheader (e.g.,user:alice@example.com) - Stored view definitions can include a security predicate -- an AST expression that references the caller's identity
- At query time, DAS resolves the identity and injects the predicate as an additional WHERE clause
- The predicate cannot be bypassed -- it is applied after the caller's own filters, not instead of them
Three Variable Resolution Strategies
- builtin -- uses request context directly (e.g.,
caller_identityfrom theX-On-Behalf-Ofheader) - direct -- uses the caller identity value as-is in the predicate
- lookup -- translates the identity through a mapping table (e.g.,
alice@example.commaps tocustomer_id: 42)
// A view definition with a security predicate.
// When user:alice queries this view, DAS automatically adds:
// WHERE customer_id = 42
// (resolved via lookup: alice@example.com -> customer_id 42)
// The agent's code doesn't change — security is transparent:
const result = await dasClient.query('firekicks', {
sql: 'SELECT * FROM my_orders WHERE order_date > $1',
params: ['2025-01-01'],
});
// Alice sees only her orders. Bob sees only his.
// The agent code is identical for both — DAS handles the filtering.
Security predicates compose with everything else: views, business rules, and the caller's own WHERE clauses all stack. A sales manager might see orders for their entire team, while a sales rep sees only their own -- same view, same agent code, different identity resolution.
// What the database actually executes for user:alice:
// SELECT * FROM orders
// WHERE customer_id = 42 -- security predicate (injected)
// AND order_status != 'cancelled' -- business rule (hard_enforced)
// AND order_date > '2025-01-01' -- caller's filter
Scratch Pad: Working Memory for Data Analysis
Real analysis is rarely a single query. An agent answering "Which of our top 100 customers have decreased their order frequency this quarter?" needs to:
- Find the top 100 customers by lifetime value
- Calculate each customer's order frequency this quarter
- Compare against their historical average
Steps 2 and 3 depend on step 1's results. The scratch pad is DAS's answer to this problem: per-identity SQLite databases that store intermediate results for use in subsequent queries.
How It Works
- Add
saveAs: "my_results"to any AST query request - DAS executes the query, then saves the results to a table called
my_resultsin the caller's scratch pad - Subsequent queries can reference the scratch pad as a connection:
scratch:user:alice - Results persist across requests within a session and can be overwritten (idempotent)
import { DataAccessClient } from '@firefoundry/data-access-client';
const dasClient = new DataAccessClient({
serviceUrl: process.env.FF_DATA_SERVICE_URL,
});
// Step 1: Find top customers, save to scratch pad
const topCustomers = await dasClient.queryAST('firekicks', {
select: {
columns: [
{ expr: { column: { column: 'customer_id' } } },
{ expr: { column: { column: 'first_name' } } },
{ expr: { column: { column: 'lifetime_value' } } },
],
from: { table: { table: 'customers' } },
orderBy: [{ expr: { column: { column: 'lifetime_value' } }, dir: 'SORT_DESC' }],
limit: 100,
},
saveAs: 'top_customers', // Saved to scratch:user:alice
});
// Step 2: Query the scratch pad results alongside live data
const result = await dasClient.queryAST('firekicks', {
select: {
columns: [
{ expr: { column: { table: 'tc', column: 'customer_id' } } },
{ expr: { column: { table: 'tc', column: 'first_name' } } },
{ expr: { function: { name: 'count', args: [{ star: {} }] } }, alias: 'order_count' },
],
from: { table: { table: 'orders', alias: 'o' } },
joins: [{
type: 'JOIN_INNER',
table: { table: 'top_customers', alias: 'tc', schema: 'scratch' },
on: {
binary: {
op: 'BINARY_OP_EQ',
left: { column: { table: 'o', column: 'customer_id' } },
right: { column: { table: 'tc', column: 'customer_id' } },
},
},
}],
groupBy: [
{ expr: { column: { table: 'tc', column: 'customer_id' } } },
{ expr: { column: { table: 'tc', column: 'first_name' } } },
],
},
});
Scratch pad limits keep things bounded: maximum 1,000 rows per staged result, 10MB total staged data, 10 staged queries per request. This is working memory, not a data warehouse.
Named Entity Recognition: Resolving Business Terms to Data Values
When a user says "Show me Nike orders," the agent needs to map "Nike" to the actual database value -- which might be "NIKE, INC." in the vendor table. Named Entity Recognition (NER) in DAS handles this through value stores with fuzzy matching.
Value stores are indexed, searchable tables of canonical values pulled from your source databases. When the agent needs to resolve a term, DAS searches across six matching strategies, each with a different weight:
- Prefix match (weight 500) -- "Nike" matches "NIKE, INC."
- Levenshtein distance (weight 400) -- "Microsft" matches "Microsoft Corp" (typo tolerance)
- Initials (weight 400) -- "NB" matches "New Balance"
- Reverse initials (weight 300) -- "MSFT" matches "Microsoft Corp" (acronym detection)
- Word Jaccard (weight 200) -- "New Balance Athletics" matches "New Balance" (partial word overlap)
- Phonetics (weight 100) -- "Addidas" matches "Adidas" (pronunciation-based)
Results are returned as ranked candidates with scores, so the agent can take the top match or present options to the user.
const resolution = await dasClient.resolveValues({
domain: 'product',
queries: [
{ term: 'Nike', entityTypes: ['Vendor'] },
{ term: 'NB', entityTypes: ['Vendor'] },
],
maxCandidates: 5,
minScore: 0.3,
});
// resolution.results[0] — "Nike":
// candidates: [
// { value: "NIKE, INC.", score: 0.95, strategy: "prefix" },
// { value: "NIKE SUBSIDIARIES", score: 0.82, strategy: "prefix" },
// ]
// resolution.results[1] — "NB":
// candidates: [
// { value: "New Balance", score: 0.88, strategy: "initials" },
// ]
Personalized Scopes
Personalized scopes add a learning layer. NER resolves values in priority order:
- User scope -- terms this specific user has confirmed before
- Team scope -- terms confirmed by the user's team
- System scope -- automatically promoted after 3+ distinct users confirm the same mapping
- Primary scope -- raw source data values
The learning loop is simple: when the agent resolves a term and the user confirms the match, confirm-match records the association. After enough users confirm the same mapping, it becomes system knowledge. Over time, the system gets better at resolving your organization's vocabulary -- without any manual configuration.
// After the user confirms "MSFT" means Microsoft Corp:
await dasClient.confirmMatch({
term: 'MSFT',
valueRowId: 456,
storeName: 'vendors',
});
// Next time anyone resolves "MSFT", the confirmed match
// is ranked higher. After 3+ users confirm the same mapping,
// it auto-promotes to system scope — visible to everyone.
Cross-Database Federation: Combining Data Across Backends
The question that every multi-database organization eventually asks: "Can I join data from PostgreSQL and MySQL in a single query?" With DAS staged queries, the answer is yes.
Staged queries let you define a set of sub-queries, each targeting a different database connection. DAS:
- Builds a dependency graph between the queries
- Groups independent queries into execution tiers (parallel execution)
- Executes each tier, collecting results
- Injects results from earlier tiers as VALUES CTEs into later queries
- Handles dialect-specific serialization to ensure correct syntax for each backend
// Example: Join customer data from PostgreSQL warehouse
// with CRM activity logs from MySQL
const result = await dasClient.queryAST('analytics', {
stagedQueries: [
{
alias: 'warehouse_customers',
connection: 'pg-warehouse', // PostgreSQL
query: {
columns: [
{ expr: { column: { column: 'customer_id' } } },
{ expr: { column: { column: 'name' } } },
{ expr: { column: { column: 'segment' } } },
{ expr: { column: { column: 'lifetime_value' } } },
],
from: { table: { table: 'customers' } },
where: {
binary: {
op: 'BINARY_OP_GT',
left: { column: { column: 'lifetime_value' } },
right: { literal: { numberValue: 10000 } },
},
},
},
},
{
alias: 'crm_activity',
connection: 'mysql-crm', // MySQL
query: {
columns: [
{ expr: { column: { column: 'customer_id' } } },
{ expr: { column: { column: 'last_contact_date' } } },
{ expr: { column: { column: 'contact_type' } } },
],
from: { table: { table: 'activity_log' } },
where: {
binary: {
op: 'BINARY_OP_GT',
left: { column: { column: 'last_contact_date' } },
right: { literal: { stringValue: '2025-01-01' } },
},
},
},
},
],
// Main query joins the staged results
select: {
columns: [
{ expr: { column: { table: 'wc', column: 'name' } } },
{ expr: { column: { table: 'wc', column: 'segment' } } },
{ expr: { column: { table: 'ca', column: 'last_contact_date' } } },
{ expr: { column: { table: 'ca', column: 'contact_type' } } },
],
from: { table: { table: 'warehouse_customers', alias: 'wc' } },
joins: [{
type: 'JOIN_LEFT',
table: { table: 'crm_activity', alias: 'ca' },
on: {
binary: {
op: 'BINARY_OP_EQ',
left: { column: { table: 'wc', column: 'customer_id' } },
right: { column: { table: 'ca', column: 'customer_id' } },
},
},
}],
orderBy: [{ expr: { column: { table: 'ca', column: 'last_contact_date' } }, dir: 'SORT_DESC' }],
},
});
Both staged queries are in tier 0 (independent), so they execute in parallel. The main query runs after both complete, with their results injected as CTEs. The agent gets a single unified result set -- no manual ETL, no temporary tables in the warehouse, no cross-database driver juggling.
The response includes stagedStats with execution details for each sub-query:
{
"stagedStats": {
"stagedQueryCount": 2,
"totalStagedRows": 245,
"details": [
{
"alias": "warehouse_customers",
"connection": "pg-warehouse",
"tier": 0,
"rowCount": 150,
"durationMs": 32
},
{
"alias": "crm_activity",
"connection": "mysql-crm",
"tier": 0,
"rowCount": 95,
"durationMs": 28
}
]
}
}
The Complete Picture
Across three articles, we have covered the full Data Access Service architecture. Here is how the pieces fit together:
- Part 1 established the foundation: unified access to 12+ backends, a semantic dictionary with business names and annotations, query analysis with EXPLAIN, and semantic views that reshape data without touching source systems.
- Part 2 added business intelligence: ontologies that map business concepts to data, process models that encode tribal knowledge as enforceable rules, and AST-based queries that eliminate SQL injection by construction.
- Part 3 completed the enterprise story: row-level security that agents cannot bypass, scratch pad working memory for multi-step analysis, named entity recognition that learns your organization's vocabulary, and cross-database federation that joins data across backends in a single query.
DAS is not a database proxy. It is a data intelligence platform that makes enterprise data AI-ready -- governed, understood, and accessible across every backend your organization uses.
The Data Access Service Series
Getting Started
The Data Access Service is available to all FireFoundry beta partners. To start using it:
- Install the client:
npm install @firefoundry/data-access-client - Configure a connection: Register your database in the FireFoundry console with its connection details, permissions, and timeout policies.
- Add semantic annotations: Use the console's dictionary editor to annotate your tables and columns with business names, descriptions, semantic types, and tags.
- Start querying: Instantiate a
DataAccessClientand call the methods described in this series. The client handles connection management, authentication, and backend translation.
For a hands-on walkthrough, the Query Explainer tutorial builds a complete agent that uses DAS to explain SQL query performance step by step. The AI-Powered Data Science tutorial shows how to combine DAS with a code sandbox to let agents run data analysis pipelines against live databases.
Ready to connect your data? If you are already in the beta, configure a connection in the console and start querying today. If you are not yet in the beta, request access and we will get you set up.