Skip to main content

Atlas v1.3: Security Graph for Databases, Atlas Scripts, the atlas cloud Command, and More

· 29 min read
Ariel Mashraki
Building Atlas

Hey everyone!

We're excited to announce Atlas v1.3. This release delivers on a promise we made in v1.2: the security graph is now live in Atlas Cloud, mapping your entire database attack surface. It also introduces Atlas Scripts for running data operations as code, safer and faster migrations, and a new command group for working with the Atlas Registry from the CLI.

Here is what you can find in this release:

  • Security Graph for Databases - Map your entire database attack surface: permissions, misconfigurations, and CVEs across every object, with severity tiers and blast-radius tracing.
  • Atlas Scripts: DataOps as Code - Backfills, GDPR purges, and reports declared as code: transactional mutations, masked queries, and batched loops, reviewed and tested in CI.
  • atlas cloud: Manage Registry Resources from the CLI - Repositories, databases, and deployment history from the terminal, with JSON output for scripts, CI gates, and AI agents, plus security graph and lineage graph exports.
  • Schema Annotations - Attach type-safe metadata to schema resources and render GraphQL, OpenAPI, or any other format from it with templates.
  • Pre-Apply Drift Detection - Block a deployment when the database drifted from the expected state, with the diff printed before any statement runs.
  • Database Driver Improvements - Lock-safe NOT NULL and 8x-10x faster schema tests on PostgreSQL, Oracle roles and storage, Snowflake Iceberg tables, ClickHouse row policies, and more.
  • Simpler Licensing and No AI Training - One agreement replaces the separate EULA and SaaS terms, with a written commitment that we never train AI models on your data.

To download and install the latest release of the Atlas CLI, simply run the following in your terminal:

curl -sSf https://atlasgo.sh | sh

Security Graph for Databases

Who can access what, and why? Answering that usually means cross-referencing GRANT statements and role memberships scattered across dozens of migration files, and access does not stop at tables: functions, triggers, views, and extensions carry privileges of their own. The security graph replaces that manual audit by mapping your entire database attack surface:

  • Trace full access paths across users, roles, and schema objects.
  • Map every function, trigger, view, and extension along with their privileges.
  • Identify extension CVEs, unmaintained code, and direct usage.
  • Surface missing RLS, PUBLIC grants, and excessive privileges.

Every object in your code or your database is plotted on one graph, with grant and inheritance relationships as edges. Analyzers run on every push and color each object by its highest-severity finding, from CVEs scored by CVSS to privilege-escalation paths such as SECURITY DEFINER functions. Selecting an object highlights its blast radius: everything it affects or is affected by, traced transitively through grants and inheritance.

Atlas Cloud Security Graph highlighting the blast radius of a selected role

Selecting a role highlights everything it can reach

The graph works for schema repositories, migration directories, and monitored databases, so it reflects what a change is about to introduce, not just what is already live in production. It is also available from the CLI as a nodes and edges JSON graph, ready for a CI gate or an AI agent's context:

atlas cloud repo secgraph --slug my-repo | jq '.nodes[] | select(.reports[]?.level == "critical")'

To learn more, see the security graph documentation and the Security as Code guides.

Atlas Scripts: DataOps as Code

Schema migrations change the shape of your database. Atlas Scripts, introduced in this release, change or read the data. A migration is applied once per database, in order, and recorded in the migration history. A script runs whenever invoked and leaves no trace in that history. For this reason, it carries its own guards: condition gates, assertions, and batch bounds. Atlas Scripts are similar to Terraform Actions, but for your database: the Day 2 work that comes after the schema is in place.

Scripts come in three kinds, each with its own command: atlas script loop runs batched work with pacing and back-pressure controls, atlas script query runs reads with output masking, and atlas script exec runs transactional mutations. Each run streams a per-step report of everything it did, and --quiet prints only the script's output:

purge.script.hcl
script "loop" "purge_inactive" {
iterator "keyset" {
cursor {
id = int
}
init {
sql = "SELECT id FROM users WHERE active = 0 ORDER BY id LIMIT 500"
}
next {
sql = "SELECT id FROM users WHERE active = 0 AND id > ? ORDER BY id LIMIT 500"
args = [cursor.id]
}
}
# Runs once per page, each iteration in its own transaction.
do {
exec {
sql = "DELETE FROM users WHERE id IN (SELECT value FROM json_each(?))"
args = [jsonencode(iterator.keyset.batch[*].id)]
}
}
}
Example execution
atlas script loop --url "sqlite://purge.db" --file "file://purge.script.hcl" --run purge_inactive

Atlas streams a per-step report as the loop runs, grouping each iteration under its own header. Each iteration opens its own transaction, the default PER_ITERATION mode, so a failure rolls back one batch rather than the whole run. Reading the table back shows the inactive users gone and the active ones untouched:

sqlite3 purge.db "SELECT count(*) FROM users WHERE active = 0; SELECT count(*) FROM users"
0
3

Because scripts are code, they are versioned, reviewed, and tested in CI before touching production data. The testing framework runs them on a real dev database and verifies both the result a script produces and the privileges it runs with, using the as block to execute under a reduced role.

Masking Sensitive Output

The result of a query may contain columns that must not appear in a report, an export, or an agent's context: email, ssn, phone, or a *_enc column. A mask block redacts those columns before the result leaves Atlas, with four methods. Each tab below masks one column of the row (1, 'ada@example.com', '123-45-6789', '4111111111111111', '555-0100'):

REDACT replaces the whole value with a fixed token (*** by default):

script "query" "u" {
query "u" {
sql = "SELECT id, email, ssn FROM users WHERE id = 1"
format = CSV
mask {
columns = ["email"]
method = REDACT
}
}
}
1,***,123-45-6789

HTTP Operations

Deleting data is rarely one DELETE statement. A user's rows have copies beyond the database: a document in the search index, an object in blob storage, a record in a downstream service, and the deletion is complete only when every copy is gone. Meta built an entire framework, DELF, around this problem: developers reliably identify what must be deleted, and reliably miss where the copies live. Scripts express the cascade as a loop: page through the deleted rows, call each service that holds a copy, and mark progress only after it confirms:

purge_deleted.script.hcl
script "loop" "purge_deleted" {
iterator "keyset" {
# ... pages through rows marked deleted, as in the batched deletion above.
}

do {
# Drop the batch from the search index first.
http "search" {
url = "https://search.internal/documents/delete"
method = POST
body = jsonencode({ ids = iterator.keyset.batch[*].id })
expect_status = 200
}
# Mark the rows purged only after the call succeeds.
exec {
sql = "UPDATE users SET purged = 1 WHERE id IN (SELECT value FROM json_each(?))"
args = [jsonencode(iterator.keyset.batch[*].id)]
}
}
}

To learn more, see the Atlas Scripts documentation.

atlas cloud: Manage Registry Resources from the CLI

The Atlas Registry already knows which repositories exist, which version every database runs, whether it is in sync, and how each deployment ended. Until this release, all of it lived behind the Atlas Cloud UI, which is built for a person looking at a screen. Ask "is prod on the latest version?" from a CI job or a coding agent, and there was nothing to read.

The new atlas cloud command group answers from the terminal. It is the same Registry state the UI renders, exposed as commands: atlas cloud repo lists, describes, and creates repositories, atlas cloud database shows deployment targets, and atlas cloud migration inspects deployment events.

A database is a deployment target with a current version and a sync status: SYNCED, PENDING, or FAILED. List them, optionally filtering by environment:

atlas cloud database list
ID   NAME                 ENV         STATUS   CURRENT VERSION
1 prod production SYNCED 20260514083000
2 prod-eu production PENDING 20260512141500
3 staging staging FAILED 20260515093000
--------------------------------
Page: 1 Page Size: 3 Total: 18

That is a fleet check in one line: prod-eu is behind, and staging failed its last deployment. To drill into one target and see when it was last deployed, use atlas cloud database describe --id 1.

Every command prints a table for a person by default, and JSON for a script, a CI gate, or an agent that asks for it. List commands are paginated and filterable.

The lineage graph grew this cycle as well. Redshift joins PostgreSQL, MySQL, ClickHouse, CockroachDB, and Snowflake as a supported engine, nodes expose quick actions for expanding, collapsing, and toggling columns across related nodes, and ClickHouse nodes display engine metadata such as table engine, projections, and ordering keys.

To learn more, see the deployment documentation, the security graph, and the lineage graph.

Schema Annotations

External tools rarely consume database schemas directly: a GraphQL server wants SDL, a REST gateway wants OpenAPI, a data catalog wants its own ingest format. Maintaining each spec alongside the schema means two sources of truth that drift apart the moment a column is renamed. Schema annotations collapse that: attach type-safe metadata to tables, columns, or any schema resource, and render whatever format the consuming tool expects:

Declare the annotation shape once on the schema's data source:

atlas.hcl
data "hcl_schema" "app" {
path = "schema.hcl"
annotation "table" {
attr "type_name" {
type = string
}
attr "description" {
type = string
}
}
}

Resources then attach concrete values, and the same type-checker that validates the schema validates them on every load:

schema.hcl
table "users" {
schema = schema.public
annotation {
type_name = "User"
description = "A registered user"
}
column "id" {
null = false
type = int
annotation {
graphql_type = "ID!"
}
}
// ...
}

Pre-Apply Drift Detection

Versioned migrations rely on deterministic execution: each migration file is written, reviewed, and tested against the exact state the migration history says the database is in. A manual hotfix breaks that silently, and the next deployment fails or makes things worse. Pre-apply drift detection verifies the guarantee inside the deployment itself.

Add a check "migrate_apply" block to the environment; before any statement runs, Atlas verifies the database matches the expected state at the latest applied revision:

atlas.hcl
env "prod" {
url = env("DATABASE_URL")
migration {
dir = "atlas://my-app"
}
check "migrate_apply" {
drift {
on_error = FAIL
}
}
}

When the target database differs from the expected state at the latest revision, the apply is aborted before any migration file runs:

Output
Executing pre-execution check (1 check in total):

-- check at atlas.hcl:10 (drift):
-> check drift against version 20260423120000

--- expected state
+++ actual state
@@ -0,0 +1,3 @@
+CREATE TABLE "audit_log" (
+ "id" integer NULL
+);

Error: database state does not match expected state at version "20260423120000"

-------------------------------------------

database state does not match expected state at version "20260423120000"

The diff is rendered in the apply transcript so the operator can see exactly which objects drifted. The migration files themselves are never executed.

Declarative migrations get the same gate with pre-execution checks.

Database Driver Improvements

Beyond the headline features, this release deepens coverage across the supported engines:

PostgreSQL

Two everyday pains got improved: NOT NULL migrations that lock or fail, and schema tests that slow down as the schema grows:

Adding a NOT NULL constraint to an existing column carries two risks: the migration fails if the column still holds NULLs, and SET NOT NULL takes an ACCESS EXCLUSIVE lock while it scans the table. The new not_null diff policy removes both:

atlas.hcl
diff "postgres" {
not_null {
check = true
lock_safe = true
}
}

check adds a pre-migration check that stops the deployment while the column holds NULLs, and lock_safe plans the change so it never holds an ACCESS EXCLUSIVE lock for a table scan: the constraint is added NOT VALID and validated under a lock that does not block reads or writes. Pre-migration NOT NULL checks are also available on MySQL and SQL Server. The not_null policy is part of Atlas Pro; run atlas login to get started.

Oracle

Oracle coverage deepened this cycle, from access control down to physical storage:

Roles and grants can now be defined as code next to the tables they protect. Role hierarchies use member_of, and permission blocks grant privileges at the table or column level:

schema.hcl
role "APP_READONLY" {
}

role "APP_WRITER" {
member_of = [role.APP_READONLY]
}

role "APP_PAYROLL" {
}

// Writer: full DML on EMPLOYEES
permission {
to = role.APP_WRITER
for = table.EMPLOYEES
privileges = ["INSERT", "UPDATE", "DELETE"]
}

// Payroll: may update ONLY the SALARY column
permission {
to = role.APP_PAYROLL
for = table.EMPLOYEES.column.SALARY
privileges = ["UPDATE"]
}

Atlas diffs the live database against this state and plans the exact GRANT and REVOKE statements, including column-scoped grants like UPDATE ("SALARY").

Snowflake

Snowflake coverage grew past tables to the objects around them, from account-level warehouses and volumes to tags and sequences:

Iceberg tables and external volumes are now first-class blocks. Declare the storage location once, reference it from the table, and Atlas creates the volume before the table and drops it after:

schema.hcl
external_volume "my_s3_vol" {
storage_location "S3" "s3_us_east" {
storage_base_url = "s3://my-bucket/iceberg/"
storage_aws_role_arn = "arn:aws:iam::123456789012:role/snowflake-role"
}
allow_writes = true
}

iceberg_table "events" {
schema = schema.PUBLIC
external_volume = external_volume.my_s3_vol
catalog = "SNOWFLAKE"
base_location = "events/"
column "id" {
type = NUMBER(10)
}
}

ClickHouse

ClickHouse gains governance controls, from row-level reads to query limits and credentials, plus user-defined functions compiled to WASM:

Row policies and full-text search indexes are now first-class resources: a policy block filters which rows a role or user can read, and the text index type tokenizes string columns for search. Three policies on one table, one scoped to a role and a user, one restrictive and applied to everyone, and one that covers every role except intern:

schema.ch.hcl
policy "analysts_only" {
on = table.orders
using = "region = 'us'"
to = [role.analyst, user.report_user]
}
policy "exclude_deleted" {
on = table.orders
restrictive = true
using = "is_deleted = 0"
to_all = true
}
policy "hide_pii" {
on = table.orders
using = "sensitivity < 3"
to_all_except = [role.intern]
}

Redshift

Atlas inspects, diffs, and migrates external schemas across all four source types: the AWS Glue Data Catalog (Spectrum), Kafka, Kinesis, and federated PostgreSQL. Declare an external block inside a schema block with the connection and IAM options for that source:

schema.hcl
schema "es_catalog" {
external "data_catalog" {
database = "spectrumdb"
iam_role = "arn:aws:iam::123456789012:role/spectrum"
region = "us-east-1"
catalog_role = "arn:aws:iam::123456789012:role/catalog"
}
}

schema "es_kafka" {
external "kafka" {
uri = "b-1.kafka.example.internal:9092"
authentication = NONE
}
}

A schema's source cannot be changed in place, so switching an external block's source is planned as a drop and re-create.

MySQL & MariaDB

MySQL gains partitioned tables in HCL, and both MySQL and MariaDB get invisible columns:

Partitioned tables are declared in HCL for MySQL: RANGE, LIST, HASH, and KEY, including the COLUMNS and LINEAR variants. Partitions live in a table-level partition block, where expression-based types use by { expr = ... } and column-based types use columns = [...]:

schema.my.hcl
table "range_orders" {
schema = schema.public
column "id" {
null = false
type = int
}
partition {
type = RANGE
by {
expr = "(`id` + 1)"
}
partition "p0" {
values_less_than = ["101"]
comment = "first range"
}
partition "pmax" {
values_less_than = ["MAXVALUE"]
comment = "catch all range"
}
}
}

SQL Server

Table partitioning adds three constructs: partition_function, partition_scheme, and a partition block inside table. This is what Atlas inspects back from an Orders table partitioned on OrderDate across three boundary values:

schema.ms.hcl
partition_function "PF1" {
input = int
values = ["100", "200", "300"]
}
partition_scheme "PS1" {
partition = partition_function.PF1
filegroups = ["PRIMARY", "PRIMARY", "PRIMARY", "PRIMARY"]
}
table "Orders" {
schema = schema.dbo
column "OrderID" { null = false, type = int }
column "OrderDate" { null = false, type = int }
primary_key { columns = [column.OrderID, column.OrderDate] }
partition {
scheme = partition_scheme.PS1
column = column.OrderDate
}
}

Changing only the boundary values is diffed into in-place SPLIT RANGE and MERGE RANGE operations rather than a drop and recreate, and both are reversible with migrate down.

New Databases

Azure HorizonDB joins the driver list, YugabyteDB graduates from beta to GA, and Aurora DSQL gains IAM token generation in atlas.hcl:

Azure HorizonDB, Microsoft's distributed PostgreSQL-compatible database, is supported through the horizondb:// driver across schema inspect, declarative apply, and versioned migrations. HorizonDB requires SSL, so include sslmode=require in the URL:

atlas schema apply \
-u "horizondb://user:pass@host:5432/app?sslmode=require" \
--to file://schema.hcl \
--dev-url "docker://postgres/17/dev"

Simpler Licensing and No AI Training

Atlas is now licensed under a single Master License and Services Agreement: one agreement covering both the software (Atlas) and Atlas Cloud, replacing the separate EULA and SaaS agreements. New and free users are governed by the MSA; existing customers stay on their current agreements. It also puts our AI data-handling policy in writing: we do not train AI models on your data, and neither do our model providers, answering a concern that comes up in nearly every security review.

What's Next

This release also ships declarative schema backups, extending the Registry backups introduced in v1.2 to the declarative workflow, as promised; atlas config validate for static validation of atlas.hcl in CI and AI authoring loops; declarative renames with renamed_from; and an IF NOT EXISTS diff policy. We ship improvements continuously between releases, so follow the Atlas changelog to stay up to date.

Here is what we are working on next:

  • Continued investment in database security, shifting detection further left into CI and pre-merge checks: more analyzers and broader engine coverage for the security graph.
  • More control over Day 2 data operations, building on Atlas Scripts.
  • Expanded database support, including NoSQL databases: MongoDB, Amazon DocumentDB, DynamoDB, and others.

Wrapping Up

Atlas v1.3 delivers what we promised in v1.2: the security graph for your databases, and backup storage that now covers the declarative workflow. It is also a first step into data operations: with Atlas Scripts, the Day 2 work that follows a schema change is versioned, reviewed, and tested like the migrations before it.

We'd love to hear your feedback! Join our Discord server or schedule a demo.