Goal: Stand up a versioned datom project whose data
lives directly in Amazon S3 and onboard data using
datom_sync(). The same progressive workflow from Getting Started applies here – one file,
update, no-op, batch – with the only difference being which store you
build. This is the “start me in object storage” path.
Want to dabble locally first? Start with Getting Started instead – it uses a local filesystem store and needs no AWS account. The two paths use the same functions; the only difference is which store you build. You can read either one first.
You are the data engineer for STUDY-001, a Phase II clinical trial, and your team already works in S3. There is no reason to stage data on a laptop: you want the very first extract to land in the shared object store, versioned from commit one. datom supports that directly. Metadata still lives in a git repository (so version history is diff-able and reproducible across machines); only the choice of data store changes.
Two locations, two roles:
datom_sync() onboards a file, storage holds the versioned,
content-addressed copy in S3. The original input file is no longer
needed; the onboarded data stands on its own.datom keeps metadata in git and data wherever you tell it to live. For this article that is an S3 bucket. You need three things:
repo. Store it in your OS keychain once with
keyring::key_set("GITHUB_PAT"). The gh CLI is
not required – datom creates the metadata repo through
the GitHub REST API directly using your PAT.adam/, tlf/).Everything machine-specific lives here. Credentials have several valid sources, so they get their own section (and their own chunks) below; set the rest once:
library(datom)
library(fs)
# --- Settings you control --------------------------------------------------
project_name <- "STUDY_001" # logical project name (recorded in metadata)
repo_name <- "study-001-data" # GitHub repo name for the metadata repo
bucket <- "study-001-data" # an S3 bucket you can read/write
# (datom does NOT create buckets)
prefix <- NULL # raw data at the bucket root; use e.g.
# "adam/" for a derived-products prefix
region <- "us-east-1" # the bucket's AWS region
# Local working directory for the metadata git clone. The data itself never
# lands here -- it goes straight to S3. A temp dir is fine for this walkthrough.
dev_dir <- path(tempdir(), "study_001_dev")
# GitHub PAT (scoped to `repo`), read from your OS keychain by name.
github_pat <- keyring::key_get("GITHUB_PAT")
# ---------------------------------------------------------------------------datom_store_s3() takes access_key and
secret_key as plain strings. datom never reads them from
the environment on your behalf – you pass the values in explicitly.
Run exactly one of the chunks below, whichever fits
your environment, then continue.
# Option A -- keyring (recommended for an interactive developer machine)
access_key <- keyring::key_get("AWS_ACCESS_KEY_ID")
secret_key <- keyring::key_get("AWS_SECRET_ACCESS_KEY")A store bundles the addresses datom needs: where parquet bytes go and the GitHub PAT that lets datom push metadata. The data component is now an S3 component instead of a local one.
Governance is not attached
(governance = NULL). A solo project’s location authority is
its own project.yaml; you do not need a portfolio register
to get started. Governance is opt-in and added later (see Governance and migration
come later).
data_component <- datom_store_s3(
bucket = bucket,
prefix = prefix,
region = region,
access_key = access_key,
secret_key = secret_key
)
store <- datom_store(
governance = NULL,
data = data_component,
github_pat = github_pat
)By default datom_store_s3() validates connectivity to
the bucket as the store is built, so a credential or permission problem
surfaces immediately rather than at first sync.
The local working directory (dev_dir) holds only the
git clone – the metadata repository. The data itself
never lands there; it goes straight to S3.
datom_init_repo(
path = dev_dir,
project_name = project_name,
store = store,
create_repo = TRUE,
repo_name = repo_name
)This creates a GitHub repo, clones it into dev_dir, and
commits a project.yaml that records the project’s S3 data
store address. No parquet data is pushed to GitHub – only metadata
commits travel over the wire.
datom_init_repo() also creates an
input_files/ directory inside the clone. It is gitignored –
files placed there are never committed. It is the inbox for
datom_sync().
conn <- datom_get_conn(path = dev_dir, store = store)
print(conn)
#> -- datom connection
#> * Project: "STUDY_001"
#> * Backend: "s3"
#> * Role: "developer"
#> * Data root: "study-001-data"
#> * Data region: "us-east-1"
#> * Governance: not attached
#> * Path: "/tmp/.../study_001_dev"
#> * Data repo: <https://github.com/.../study-001-data>The month-1 extract has just landed. Drop it in the input folder:
# The input folder lives inside the git clone but is gitignored.
# Files placed here are the raw material for datom_sync().
input_dir <- path(dev_dir, "input_files")
write.csv(
datom_example_data("dm", cutoff_date = "2026-01-28"),
path(input_dir, "dm.csv"),
row.names = FALSE
)Scan and sync:
manifest <- datom_sync_manifest(conn)
#> i Scanned 1 file: 1 new, 0 changed, 0 unchanged.
datom_sync(conn, manifest)
#> i Syncing 1 table...
#> v dm synced (new).
#> i Sync complete: 1 succeeded, 0 failed, 0 skipped.Three things just happened, in order:
metadata.json and version_history.json
were updated in the git clone and committed.Confirm:
datom_list(conn)
#> name current_version current_data_sha last_updated
#> 1 dm a8ee7a31 4b6d0a7e 2026-01-28T...
datom_history(conn, "dm")
#> version data_sha timestamp message
#> 1 a8ee7a31 4b6d0a7e 2026-01-28T09:02:11Z dm synced from dm.csvThe input file is now disposable – storage (S3) is the source of truth:
The month-2 extract arrives with new subjects:
write.csv(
datom_example_data("dm", cutoff_date = "2026-02-28"),
path(input_dir, "dm.csv"),
row.names = FALSE
)
manifest <- datom_sync_manifest(conn)
#> i Scanned 1 file: 0 new, 1 changed, 0 unchanged.
datom_sync(conn, manifest)
#> i Syncing 1 table...
#> v dm synced (changed).
#> i Sync complete: 1 succeeded, 0 failed, 0 skipped.Two versions now coexist in S3. Read either one:
datom_history(conn, "dm")
#> version data_sha timestamp message
#> 1 5c1a3f7b 9e8f1c2d 2026-02-28T10:14:02Z dm synced from dm.csv
#> 2 a8ee7a31 4b6d0a7e 2026-01-28T09:02:11Z dm synced from dm.csv
# Current version (month 2)
nrow(datom_read(conn, "dm"))
#> [1] 16
# Prior version (month 1) by SHA
hist <- datom_history(conn, "dm")
m1_ver <- hist$version[nrow(hist)] # oldest row is the month-1 version
nrow(datom_read(conn, "dm", version = m1_ver))
#> [1] 4manifest <- datom_sync_manifest(conn)
#> i Scanned 1 file: 0 new, 0 changed, 1 unchanged.
datom_sync(conn, manifest)
#> i All files unchanged. Nothing to sync.Identical content is never re-uploaded. Safe to run on a schedule.
Month-3 brings four domains at once:
cutoff <- "2026-03-28"
write.csv(datom_example_data("dm", cutoff_date = cutoff),
path(input_dir, "dm.csv"), row.names = FALSE)
write.csv(datom_example_data("ex", cutoff_date = cutoff),
path(input_dir, "ex.csv"), row.names = FALSE)
write.csv(datom_example_data("lb", cutoff_date = cutoff),
path(input_dir, "lb.csv"), row.names = FALSE)
write.csv(datom_example_data("ae", cutoff_date = cutoff),
path(input_dir, "ae.csv"), row.names = FALSE)
manifest <- datom_sync_manifest(conn)
#> i Scanned 4 files: 3 new, 1 changed, 0 unchanged.
datom_sync(conn, manifest)
#> i Syncing 4 tables...
#> v dm synced (changed).
#> v ex synced (new).
#> v lb synced (new).
#> v ae synced (new).
#> i Sync complete: 4 succeeded, 0 failed, 0 skipped.All four tables are now versioned in S3:
The reader role connects with bucket credentials alone – no PAT, no git clone:
reader_store <- datom_store(
governance = NULL,
data = datom_store_s3(
bucket = bucket,
prefix = prefix,
region = region,
access_key = access_key,
secret_key = secret_key
)
) # no PAT -> reader role
reader_conn <- datom_get_conn(store = reader_store, project_name = project_name)
datom_read(reader_conn, "lb") # labs, streamed directly from S3The read streams the parquet object from S3 directly; it does not go through GitHub. A teammate on another machine takes the same path – they need access to the bucket, not to the git repo.
project.yaml.To trace how derived tables descend from their sources, see Tracing Data Lineage.
Two capabilities are deliberately not part of this starting story:
datomanager),
adopted when a project graduates from solo to shared/managed. Starting
on S3 does not commit you to it.In other words, start-on-S3 and migrate-to-S3 are complementary entry points: this article is the greenfield path, and managed migration is the move-an-existing-project path.
When you are done exploring, tear the project down.
datom_repo_delete() removes the GitHub metadata repo and
the local clone. Do this before deleting the local
directory by hand – removing the clone first strips the GitHub remote
reference and the remote repo will not be deleted.
datom_repo_delete() does not empty your
S3 bucket – bucket lifecycle is your organization’s domain, not datom’s.
Remove the project’s objects from S3 with your own tooling (for example,
the AWS CLI) if you no longer need them.