Problem
Stack.create() stamps createdAt and updatedAt from new Date() unconditionally (packages/core/src/stack.ts), and the wire refuses client-supplied timestamps by design (docs/spec/wire-format.md § Records). There is therefore no supported way to import an existing corpus with its real dates: every record lands stamped with the import moment, which destroys chronological ordering, feed dates, archive pages, and any date-range query over the imported data.
This is not a niche path. "Take your data with you" is the project's stated purpose, and the inbound direction currently loses the one fact that makes an archive an archive. Every migration into a stack from anywhere hits it.
The sharpest framing: the ID already carries this
Record IDs are Crockford base-32 with a 9-character millisecond prefix, they are client-mintable by design, and the ID is the sort key. docs/spec/data-model.md § Record IDs is explicit that the prefix "decodes to the creation millisecond exactly."
So core already lets an owner choose a record's creation position. It just won't let the createdAt field agree with it — and then the two can silently disagree, with nothing detecting it.
That makes this an inconsistency to close rather than a new capability to grant, which is a considerably smaller thing to argue about.
Proposed scope
Stack.create(typeId, content, { createdAt, updatedAt }) — unscoped Stack only.
- Never on
ScopedStack. The ID timestamp-skew check exists precisely because a grantee could otherwise forge a sort position; a settable createdAt there would route straight around it.
- Never over the wire.
POST /records keeps refusing both, unchanged.
- Invariant: supplying an
id and a createdAt that disagree beyond the configured skew tolerance is an error, not a silent divergence. The whole point is to make the two agree.
updatedAt matters as much as createdAt. The create-then-update workaround bumps the record to version 2 and stamps updatedAt: now, so one without the other doesn't solve the problem — and it also pollutes version history with a fake revision per record.
Alternative considered
Write below Stack directly to the record adapter during import, constructing full records by hand. It works — adapter.createRecord(record) takes the whole record — and it is what an importer has to do today. But it skips content validation, DID-binding checks, reserved-key rejection and attachment mimeType establishment: exactly the invariants docs/spec/adapters.md says live in Stack so every adapter inherits them. A supported option keeps imports on the validated path instead of teaching every importer to go around it.
Concrete driver
The cuibonobo.com migration — years of dated posts moving from D1 into a stack. Record IDs carry over unchanged (core's id.ts is a direct port of that project's, same alphabet and layout), so the ordering is recoverable from the IDs; the timestamps are not.
It is most acute for short-form published entries, which under the commons' own clock rules carry no content date at all — message@1 has no date field because "meaningful time is createdAt" — so native createdAt is the only ordering key they will ever have. A commons post@1 proposal with the same property is open separately.
Problem
Stack.create()stampscreatedAtandupdatedAtfromnew Date()unconditionally (packages/core/src/stack.ts), and the wire refuses client-supplied timestamps by design (docs/spec/wire-format.md § Records). There is therefore no supported way to import an existing corpus with its real dates: every record lands stamped with the import moment, which destroys chronological ordering, feed dates, archive pages, and any date-range query over the imported data.This is not a niche path. "Take your data with you" is the project's stated purpose, and the inbound direction currently loses the one fact that makes an archive an archive. Every migration into a stack from anywhere hits it.
The sharpest framing: the ID already carries this
Record IDs are Crockford base-32 with a 9-character millisecond prefix, they are client-mintable by design, and the ID is the sort key.
docs/spec/data-model.md § Record IDsis explicit that the prefix "decodes to the creation millisecond exactly."So core already lets an owner choose a record's creation position. It just won't let the
createdAtfield agree with it — and then the two can silently disagree, with nothing detecting it.That makes this an inconsistency to close rather than a new capability to grant, which is a considerably smaller thing to argue about.
Proposed scope
Stack.create(typeId, content, { createdAt, updatedAt })— unscopedStackonly.ScopedStack. The ID timestamp-skew check exists precisely because a grantee could otherwise forge a sort position; a settablecreatedAtthere would route straight around it.POST /recordskeeps refusing both, unchanged.idand acreatedAtthat disagree beyond the configured skew tolerance is an error, not a silent divergence. The whole point is to make the two agree.updatedAtmatters as much ascreatedAt. The create-then-update workaround bumps the record to version 2 and stampsupdatedAt: now, so one without the other doesn't solve the problem — and it also pollutes version history with a fake revision per record.Alternative considered
Write below
Stackdirectly to the record adapter during import, constructing full records by hand. It works —adapter.createRecord(record)takes the whole record — and it is what an importer has to do today. But it skips content validation, DID-binding checks, reserved-key rejection and attachment mimeType establishment: exactly the invariantsdocs/spec/adapters.mdsays live inStackso every adapter inherits them. A supported option keeps imports on the validated path instead of teaching every importer to go around it.Concrete driver
The cuibonobo.com migration — years of dated posts moving from D1 into a stack. Record IDs carry over unchanged (core's
id.tsis a direct port of that project's, same alphabet and layout), so the ordering is recoverable from the IDs; the timestamps are not.It is most acute for short-form published entries, which under the commons' own clock rules carry no content date at all —
message@1has no date field because "meaningful time iscreatedAt" — so nativecreatedAtis the only ordering key they will ever have. A commonspost@1proposal with the same property is open separately.