Data Seeding
Belt provides two ways to get data into an environment without hand-crafting rows: copying data from another environment, and Rails-style seed files.
belt db:copy — copy data between environments
Copies DynamoDB table contents from one environment into another, matching
tables by name after stripping each environment's <app>-<env>- prefix.
belt db:copy prod dev # copy prod data into dev
belt db:copy prod dev --force # overwrite dev tables even if non-empty
By default, destination tables that already contain data are skipped — safe
to re-run against a live environment. --force overwrites them instead.
Cognito identity re-anchoring
Cognito identities are per-environment: each environment has its own user
pool, so the same person has a different sub in every environment. Any row
that references a user by their sub (e.g. a membership's cognito_sub) has a
reference that's meaningless in another environment — copy it verbatim and the
row points at a user who doesn't exist in the destination pool, so it silently
disappears (a copied project you can't see, a member who isn't there).
belt db:copy handles this automatically:
- The destination's own
userstable is left untouched — the destination pool is authoritative for who its users are and whatsubeach one has. - For every other table, any row carrying both an
emailand acognito_subhas itscognito_subre-anchored to the destination user with the same email. - A row whose email has no destination user yet has its stale
cognito_subcleared, so it reads as unclaimed (e.g. a pending invitation Belt binds on that person's first login) rather than dangling.
belt db:copy prod dev # re-anchors identities (default)
belt db:copy prod dev --no-remap-identity # copy cognito_sub refs verbatim
This relies on Belt's cognito_authenticatable convention: the users table is
<app>-<env>-users, its primary key is id (the Cognito sub), it carries an
email, and cognito_sub is the foreign-key attribute referencing it. The
same re-anchoring runs in the nested (PR-preview) environment deploy hook.
Cross-account copies
Source and destination environments often live in different AWS accounts
(e.g. prod vs. dev). belt db:copy resolves the AWS profile for each side
independently from infrastructure/<env>/belt.rb (config.aws_profile):
# infrastructure/prod/belt.rb
Belt.configure do |config|
config.aws_profile = "prod-readonly"
end
Override either side explicitly if you don't want to rely on belt.rb:
belt db:copy prod dev --from-profile prod-readonly --to-profile dev
How it works
- Lists tables under each environment's prefix (
<app>-<env>-) using the AWS CLI (aws dynamodb list-tables) - Pairs up tables by matching suffix (e.g.
myapp-prod-posts↔myapp-dev-posts) - Scans the source table and
batch-write-items into the destination, re-anchoring Cognito-sub foreign keys to the destination's users by email (unless--no-remap-identity; see above) - Skips (or overwrites, with
--force) destination tables that already have items
This is the same mechanism used by nested (PR-preview) environment deploys
to seed a preview environment's tables from its parent — belt db:copy just
exposes it as a standalone command for any two environments.
belt db:seed — Rails-style seed file
Mirrors rails db:seed. Loads config/seeds.rb in the same booted context
belt console uses — your models (ActiveItem) are available, targeting the
resolved environment's tables.
belt db:seed # seeds dev, or $BELT_ENV if set
belt db:seed dev01 # explicit environment
belt db:seed prod # prompts for confirmation, like belt console prod
config/seeds.rb is a plain Ruby file:
# frozen_string_literal: true
post = Post.create!(title: "Hello, world", body: "Seeded post")
puts "Created post: #{post.id}"
Safety
belt db:seed refuses to run if the target environment's tables already
have data, to avoid clobbering a live environment (or accidentally reseeding
one that's already loaded). Pass --force to seed anyway:
belt db:seed dev01 --force
Because of this guard, seeds are typically run once against a fresh
environment. If you want seeds.rb to be safely re-runnable regardless,
write it idempotently (find_or_create_by-style) — belt db:seed --force
does not enforce idempotency for you.
Scaffolding
belt new generates a starter config/seeds.rb with usage notes and an
example. Existing apps can add the file manually — it's just a plain Ruby
file, no generator required.
See Also
belt explain backups— recovery-point snapshots, not data seedingbelt explain console— the app-booting mechanismdb:seedreusesbelt explain deployment— nested environments and the parent → child copy hook