Skip to content

RFC: ATProto compatibility layer — core type and API changes #15

@cuibonobo

Description

@cuibonobo

Motivation

As Haverstack moves toward federation and multi-stack interop, ATProto (the protocol behind Bluesky) is the most architecturally aligned target. Both are record-centric, identity-scoped, and schema-namespaced. This issue proposes a set of additive, non-breaking changes to @haverstack/core that would make a future @haverstack/adapter-atproto straightforward to implement.


Proposed changes

1. Content addressing — add cid to StackRecord

ATProto's foundational property is content addressing: every record has a CID (Content Identifier) derived from its content. Without this, Haverstack can't participate in ATProto's signed commit log or verify record integrity across stacks.

// types.ts
export type StackRecord = {
  id: RecordId;
  cid?: string; // SHA-256 CID of the content blob, computed on write
  // ...existing fields
};

The stack should compute and store cid on create and update. ATProto adapters can use this to build a proper commit chain.


2. Surface previousCid on update()

ATProto records are immutable at a given CID — updating means writing a new CID and recording the transition. The current update() return type should surface this:

async update(id: string, content: ...): Promise<StackRecord & { previousCid?: string }>

This is largely an adapter-level concern, but exposing it in the return type keeps the door open without requiring a future breaking change.


3. at:// URI support

ATProto identifies records by URI: at://<did>/<nsid>/<rkey>. Haverstack IDs are within-stack only (this is already noted in the README). Two additions:

A helper method on Stack:

getAtUri(record: StackRecord): string {
  return `at://${this.ownerEntityId}/${record.typeId}/${record.id}`;
}

atUri on RelationshipAssociation for cross-stack references:

export type RelationshipAssociation = {
  kind: 'relationship';
  label: string;
  recordId: RecordId;
  stackUrl?: string; // already implied — make explicit
  atUri?: string;    // ATProto native reference form
};

4. DID support on EntityContent

ATProto identity is built on DIDs (did:plc:... or did:web:...). Handles are resolved to DIDs; DIDs are the stable identity. EntityContent should support this natively:

export type EntityContent = {
  name: string;
  handle?: string;
  did?: string; // e.g. "did:plc:abc123" or "did:web:example.com"
};

Stack.ownerEntityId is currently a local RecordId. A companion ownerDid?: string on the Stack class (read from adapter config) would make ATProto stack identity unambiguous.


5. unlisted permission + tombstone on StackRecord

ATProto distinguishes tombstoned records (URI remains valid, content is gone) from deleted ones. It also has no direct equivalent of { access: 'public' } without a concept of "accessible by URI but not indexed."

export type Permission =
  | { access: 'public' }
  | { access: 'unlisted' } // accessible via URI, not surfaced in feeds/indexes
  | { access: 'entity'; entityId: RecordId; read: boolean; write: boolean }
  | { access: 'group'; groupId: RecordId; read: boolean; write: boolean };

export type StackRecord = {
  // ...
  tombstone?: boolean; // URI remains valid, content is redacted — distinct from deletedAt
};

6. lexiconId on StackType

ATProto Lexicons use reverse-DNS without a version suffix in the $type field (e.g. app.bsky.feed.post). Haverstack's com.example.myapp/note@1 format doesn't need to change, but storing an optional Lexicon ID alias lets an ATProto adapter map outbound records to the correct $type without apps having to change their internal naming:

// types.ts
export type StackType = {
  // ...existing fields
  lexiconId?: string; // e.g. "app.bsky.feed.post"
};

// stack.ts
export type DefineTypeOptions = {
  migratesFrom?: TypeId;
  lexiconId?: string;
};

Summary

Change Breaking?
cid on StackRecord No — optional field
previousCid on update() return No — additive to return type
getAtUri() + atUri on relationships No
did on EntityContent No — optional field
unlisted + tombstone No
lexiconId on defineType / StackType No

All changes are additive. They lay the groundwork for @haverstack/adapter-atproto without requiring any migration of existing stacks or changes to app code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions