ts-prorm-orm Migrate) plans,import { Migrate } from 'ts-prorm-orm';
const job = Migrate.TransferJob.create({
strategyId: 'sftp', // how it moves
tier: 'premium', // the account's tier gates the strategy
source: { kind: 'database', dialect: 'postgres', location: 'db://staging', region: 'eu' },
dest: { kind: 'database', dialect: 'postgres', location: 'db://prod', region: 'eu' },
bytes: 42 * 1024 ** 3,
safe: true, // keep a restorable backup
requiredControls: ['ssh-ciphers', 'key-auth', 'integrity-manifest'],
});
job.scope; // 'unscoped' — same engine, environment management
job.cost; // { totalCents, transferCents, safetyCents, … }| Group | Strategies | Tier |
|---|---|---|
| Standard encrypted | HTTPS multipart, WebDAV | pro |
| SFTP, SCP/rsync over SSH, FTPS | premium | |
| Accelerated | QUIC/HTTP-3 | pro |
| Globus/GridFTP, Aspera, Signiant, UDT | premium (high-risk) | |
| Managed File Transfer | GoAnywhere/MOVEit/Axway/… | premium |
| EDI | PGP payload | pro |
| AS2/AS4 | premium | |
| Network | VPN / private interconnect | premium |
| DB replication | native replication / CDC over TLS | premium |
| Offline | Snowball / Data Box / Transfer Appliance | premium (high-risk) |
Migrate.strategiesForTier(tier) lists what anMigrate.availability(id, tier) explains a refusal.Migrate.strategiesForTier('pro').map((s) => s.id);
// [ 'https', 'webdav', 'quic', 'pgp' ]
Migrate.availability('aspera', 'pro');
// { allowed: false, reason: 'Aspera FASP (IBM) needs a premium account; this account is pro' }classifyScope decides,Migrate.classifyScope(
{ kind: 'database', dialect: 'postgres', location: 'db://staging' },
{ kind: 'database', dialect: 'sqlite', location: 'file:./snapshot.db' },
);
// { scope: 'scoped', reasons: [ 'different engines: postgres → sqlite' ] }Migrate.estimateCost({ bytes, safe, strategyId }) quotes it before it runs.| Band | Rate |
|---|---|
| every job | $1.00 base |
| first 1 GB | 50¢/GB |
| 1–100 GB | 20¢/GB |
| 100 GB–1 TB | 8¢/GB |
| beyond 1 TB | 3¢/GB |
| safe job | +2¢/GB per day the backup is kept (7 days by default) |
| Aspera, Signiant ×2.5 · offline ×3 · UDT ×1.8 · Globus, MFT ×1.5 | multiplies the transfer charge |
import { Migrate } from 'ts-prorm-orm';
const GB = 1024 ** 3;
for (const [label, input] of [
['5 MB over SFTP', { bytes: 5 * 1024 ** 2, strategyId: 'sftp' }],
['100 GB over SFTP', { bytes: 100 * GB, strategyId: 'sftp' }],
['100 GB over SFTP, safe', { bytes: 100 * GB, strategyId: 'sftp', safe: true }],
['100 GB over Aspera', { bytes: 100 * GB, strategyId: 'aspera' }],
] as const) {
const cost = Migrate.estimateCost(input);
console.log(label.padEnd(24), Migrate.dollars(cost.totalCents));
}5 MB over SFTP $1.00
100 GB over SFTP $21.30
100 GB over SFTP, safe $35.30
100 GB over Aspera $51.75local=true. The response carries the job's klock, once:$ prorm transfer create -s db-replication \
--source kind=database,dialect=postgres,location=db://localhost/shop,local=true \
--dest kind=database,dialect=postgres,location=db://prod
! Local job: keep its klock to run it on-device (shown once):
…
✓ Job tr_ab12cd — created, unscoped, projected $1.00
Start it: prorm transfer start tr_ab12cd$ prorm transfer start tr_ab12cd
Projected price: $1.00 (charged on completion)
Run this transfer and be charged $1.00? [y/N] y
✓ Job tr_ab12cd — queued$ prorm transfer run tr_ab12cd \
--source-url postgres://app:pw@localhost/shop \
--dest-url "$DEST_URL" \
--tables users,orders,invoices
users: +1000 rows (1000 total)
…
✓ Ran tr_ab12cd on-device — 3 table(s), … bytes. Certified server-side.--source-url and --dest-url appear only on the left ofMigrate library does the moving —databaseSource/databaseSink copy a table at a time over the ORM, so.klock — signs a certificate over the manifest digest and the job'sjob.certificate carries it;job.klock.verify(cert) checks it.Authorization: Klock <sig>), so the agent needs no session token. It isprorm transfer create and kept in the agent config, not in a.klock file. How it relates to the passwordless .klock is inprorm transfer create --start and prorm transfer start show the projected--yes / -y to skip the prompt), and pass thatPUT /teams/:id/transfer-billing (immediate ordeferred); a personal job is billed immediately.[!IMPORTANT]
Without a terminal there is no one to confirm the price, soprorm transfer start tr_ab12cd(andtransfer create --start) exits 2 withNo terminal to confirm the projected price on. Re-run with --yes to accept it (e.g. in a script or CI).The job stayscreated, and nothing is charged. Pass--yeswhen a pipeline is meant to start (and pay for) the transfer. Before
4.9.2 it printedNot started.and exited 0, so a pipeline did not notice.
TRANSFER_LOCAL_FLAT_BYTES on the API),cost carries basis: local-flat when the flatpricedBytes and flatBytes), or declared.meteredBytes.receipt with its eventsPOST /transfer/jobs/:id/events). The API checks it against the verified422 if it is invalid, not completed or403 if it is not for your job, 409 if it was already503 if the API has no verified keyset.platform (a cloud job),relay-metered (relay receipts), or self-attested (your own report).self-attested as your claim, not a platform measurement.[!NOTE]
This is new in the control plane onfeat/klock-capabilities(API6b18fcc, PRORM-SA-2026-026).
Unfinished local jobs priced below the floor are re-priced by migration 0032
and again when they start, settle or re-arm, so confirming the old price
answers409with "re-quote": quote the job again and confirm the new
price.
prorm transfer drives the whole thing without writing code:key=value spec — kind (database, object-store,dialect, location, region and types+) as they apply. --bytes takes a human size20mb, 1.5gb, 2tb). --controls lists the controls the transport mustExecutor the platform supplies — a serverless functionjob.start(executor) walks thecreated → queued → [backing-up] → running → verifying →
completed, or, for a safe job that fails, failed → restoring → restored.job.toJSON() is the snapshot the status API returns, and onStatus is thebacking_up is backing-up in the API; Mermaid state names cannot carry a hyphen.)Executor is available.databaseCopyExecutor is the one a database-to-database job should use: itimport { Migrate, Prorm } from 'ts-prorm-orm';
const source = new Prorm({ dialect: 'sqlite', storage: './local.db' });
const dest = new Prorm({ dialect: 'sqlite', storage: './copy.db' });
await source.connect();
await dest.connect();
const job = Migrate.TransferJob.create({
strategyId: 'db-replication',
tier: 'premium',
source: { kind: 'database', dialect: 'sqlite', location: 'file:./local.db', local: true },
dest: { kind: 'database', dialect: 'sqlite', location: 'file:./copy.db', local: true },
bytes: 0, // unknown up front: verify against what was actually moved
onStatus: (status) => console.log('status:', status),
});
await job.start(
Migrate.databaseCopyExecutor(source, dest, ['users'], {
batchSize: 2,
onBatch: (b) => console.log('batch', b),
}),
);
console.log(job.status, job.manifest?.digest);
console.log('certificate valid:', job.klock.verify(job.certificate!));users:status: created
status: queued
status: running
batch { table: 'users', rows: 2, lastKey: 2, totalRows: 2 }
batch { table: 'users', rows: 1, lastKey: 3, totalRows: 3 }
status: verifying
status: completed
completed 117b1a88f3bc4b23032cf4275811a08faacbc53ff92e985e7a198efc15c3b6b8
certificate valid: truebatch.lastKey is the resume token: pass it back as resumeAfter and an[!WARNING] bytesis checked, not just displayed. A job created withbytes: 4096whose
manifest totals 86 bytes goesverifying → failedwithmoved 86 bytes, expected 4096, and no certificate. Pass the real size, or0when you do not
know it, which means "as measured".