ATProto Data Architecture for Aesthetic Computer #
Last Updated: October 6, 2025
TL;DR #
- ✅ PDS = Identity & Auth only (replaces Auth0)
- ✅ MongoDB = ALL AC content (paintings, moods, KidLisp, chat)
- ✅ DigitalOcean Spaces = ALL AC media (no change)
- ✅ Publishing to ATProto = Optional copies (doesn't move data)
The Hybrid Architecture #
What Changes #
| Component | Before ATProto | After ATProto |
|---|---|---|
| Authentication | Auth0 OAuth | PDS OAuth |
| User ID | auth0|123 |
did:plc:abc123 |
| Handle system | MongoDB only | MongoDB maps DID ↔ handle |
| Profile data | MongoDB only | MongoDB + synced to PDS |
What DOESN'T Change #
| Component | Storage | Notes |
|---|---|---|
| Paintings | DigitalOcean Spaces + MongoDB | Unchanged |
| Moods | MongoDB | Unchanged |
| KidLisp code | MongoDB | Unchanged |
| Chat messages | MongoDB | Unchanged |
| Pieces/projects | MongoDB | Unchanged |
| User preferences | MongoDB | Unchanged |
| Activity logs | MongoDB | Unchanged |
| Assets | DigitalOcean Spaces | Unchanged |
Data Storage Responsibilities #
1. PDS (PostgreSQL) - Identity Layer #
Stores ONLY:
- User DIDs (Decentralized Identifiers)
- ATProto authentication tokens
- ATProto-standard profile (displayName, avatar, bio)
- ATProto repository metadata
- Optional: Posts published to ATProto network
- Optional: Social graph (follows/followers) if using ATProto
Size: Very small (~10-100 MB for thousands of users)
Example:
-- PDS PostgreSQL schema (simplified)
users:
did (PK)
handle
email
password_hash
created_at
repos:
did (FK)
collection
rkey
record (JSONB)
cid
2. MongoDB - Application Data Layer #
Stores EVERYTHING AC-specific:
- User handle mappings
- Paintings metadata
- Moods
- KidLisp snippets
- Chat messages
- Pieces/projects
- User preferences
- Activity logs
- All AC-specific features
Size: Large (~GBs, current usage)
Example:
// MongoDB @handles collection
{
_id: "did:plc:abc123", // ← Primary key changed from auth0|123
handle: "jeffrey",
atprotoHandle: "jeffrey.aesthetic.computer",
email: "jeffrey@example.com",
authProvider: "atproto",
legacyAuth0Sub: "auth0|123", // For migration
created: ISODate("2023-01-01"),
// ... all other AC-specific user data
}
// MongoDB paintings collection (UNCHANGED structure)
{
_id: ObjectId("..."),
user: "did:plc:abc123", // ← Only change: DID reference
filename: "my-painting.png",
url: "https://assets.aesthetic.computer/paintings/jeffrey/my-painting.png",
created: ISODate("2025-10-06"),
atprotoUri: "at://did:plc:abc123/app.bsky.feed.post/3k2j...", // Optional
// ... all other fields unchanged
}
// MongoDB moods collection (UNCHANGED)
{
_id: ObjectId("..."),
user: "did:plc:abc123",
mood: "happy",
when: ISODate()
}
// MongoDB kidlisp_cache collection (UNCHANGED)
{
_id: ObjectId("..."),
user: "did:plc:abc123",
code: "(defn hello [] (print \"Hello world\"))",
hash: "sha256:...",
when: ISODate()
}
3. DigitalOcean Spaces - Media Storage #
Stores:
- All painting image files
- All asset files
- All user-uploaded media
- Static assets (fonts, icons, etc.)
Size: Largest (~GBs of image/media files)
Structure (UNCHANGED):
assets.aesthetic.computer/
├── paintings/
│ ├── jeffrey/
│ │ ├── my-painting-2025.png
│ │ └── abstract-work.png
│ └── alice/
│ └── digital-art.png
├── assets/
│ ├── fonts/
│ └── icons/
└── pieces/
└── user-projects/
4. PDS Blob Storage (NEW, Optional) #
Stores:
- Copies of media shared to ATProto network
- Only when user explicitly publishes content
Size: Small (~MB, only shared content)
Example:
# User shares painting to ATProto:
1. Original stays in DO Spaces: paintings/jeffrey/art.png
2. Copy uploaded to PDS blobs: blobs/bafyrei.../art.png
3. ATProto post references blob
4. Bluesky users see the image
5. AC still serves from DO Spaces
Data Flow Examples #
Example 1: User Creates Painting #
sequenceDiagram
User->>AC Client: Paint artwork
AC Client->>Session Server: Save painting
Session Server->>DigitalOcean: Upload PNG file
DigitalOcean-->>Session Server: URL
Session Server->>MongoDB: Save metadata + URL
MongoDB-->>Session Server: Saved
Session Server-->>AC Client: Success
Note over User,AC Client: Painting saved, ends here normally
User->>AC Client: Click "Share to ATProto"
AC Client->>Session Server: Publish to ATProto
Session Server->>DigitalOcean: Fetch image
DigitalOcean-->>Session Server: Image data
Session Server->>PDS: Upload blob
PDS-->>Session Server: Blob CID
Session Server->>PDS: Create post with embed
PDS-->>Session Server: Post URI
Session Server->>MongoDB: Update with atprotoUri
Session Server-->>AC Client: Published!
Note over PDS,Bluesky: Post federates to ATProto network
Example 2: User Sets Mood #
sequenceDiagram
User->>AC Client: Set mood "happy"
AC Client->>Session Server: Update mood
Session Server->>MongoDB: Save mood
MongoDB-->>Session Server: Saved
Session Server->>WebSocket: Broadcast mood
WebSocket-->>Other Users: Show mood update
Note over User,MongoDB: No ATProto interaction - AC-only feature
Example 3: User Writes KidLisp #
sequenceDiagram
User->>AC Client: Write KidLisp code
AC Client->>Session Server: Cache code
Session Server->>MongoDB: Save snippet
MongoDB-->>Session Server: Saved
Session Server-->>AC Client: Cached
Note over User,MongoDB: Code stays in MongoDB
opt User chooses to share
User->>AC Client: Share KidLisp
AC Client->>Session Server: Publish snippet
Session Server->>PDS: Create custom record
Note over PDS: Uses computer.aesthetic.kidlisp lexicon
PDS-->>Session Server: Record URI
Session Server->>MongoDB: Link to ATProto record
end
Migration Impact #
What Happens to Existing Data #
Paintings #
- ✅ Files stay in DigitalOcean Spaces (no migration)
- ✅ MongoDB metadata stays (only user field changes from auth0|123 to DID)
- ✅ URLs unchanged
- ✅ No downtime or data transfer needed
Moods #
- ✅ All records stay in MongoDB
- ✅ Only user field updated (auth0|123 → DID)
- ✅ No data migration needed
KidLisp Snippets #
- ✅ All code stays in MongoDB
- ✅ Only user field updated
- ✅ Cache remains valid
Chat Messages #
- ✅ All messages stay in MongoDB
- ✅ No changes to message structure
- ✅ WebSocket continues to work
Migration Script Example #
// Minimal migration - only update user references
async function migrateUserData(auth0Sub, newDID) {
const db = await connectMongoDB()
// Update all collections with user references
await db.collection('paintings').updateMany(
{ user: auth0Sub },
{ $set: { user: newDID } }
)
await db.collection('moods').updateMany(
{ user: auth0Sub },
{ $set: { user: newDID } }
)
await db.collection('kidlisp_cache').updateMany(
{ user: auth0Sub },
{ $set: { user: newDID } }
)
await db.collection('chat-system').updateMany(
{ user: auth0Sub },
{ $set: { user: newDID } }
)
// Update @handles collection
await db.collection('@handles').updateOne(
{ _id: auth0Sub },
{
$set: {
migratedDID: newDID,
authProvider: 'atproto'
}
}
)
// Create new primary document with DID
await db.collection('@handles').insertOne({
_id: newDID,
handle: user.handle,
atprotoHandle: `${user.handle}.aesthetic.computer`,
legacyAuth0Sub: auth0Sub,
// ... copy other fields
})
}
Content Publishing Policies #
When to Publish to ATProto #
User-controlled decisions:
- User explicitly clicks "Share to ATProto"
- User enables "Auto-publish to ATProto" in settings
- User posts to "public" visibility
What gets published:
- ✅ Posts/messages marked as public
- ✅ Paintings user chooses to share
- ✅ KidLisp snippets marked as shareable
- ✅ Profile updates (synced automatically)
What NEVER gets published:
- ❌ Private chat messages
- ❌ Moods (AC-internal feature)
- ❌ Draft/private paintings
- ❌ Personal preferences/settings
- ❌ Activity logs
ATProto Record Types #
// Standard Bluesky post
{
$type: 'app.bsky.feed.post',
text: 'Check out my new painting!',
embed: {
$type: 'app.bsky.embed.images',
images: [{ image: blob, alt: 'My artwork' }]
},
createdAt: '2025-10-06T12:00:00Z'
}
// Custom AC lexicon for paintings (optional)
{
$type: 'computer.aesthetic.painting',
title: 'Abstract Dreams',
description: 'Digital painting created in AC',
media: { blob: '...', mimeType: 'image/png' },
originalUrl: 'https://assets.aesthetic.computer/paintings/...',
tools: ['brush', 'stamp'],
createdAt: '2025-10-06T12:00:00Z'
}
// Custom AC lexicon for KidLisp
{
$type: 'computer.aesthetic.kidlisp',
code: '(defn hello [] (print "Hello world"))',
description: 'Simple greeting function',
language: 'kidlisp',
createdAt: '2025-10-06T12:00:00Z'
}
Storage Costs Breakdown #
Current (Before ATProto) #
| Service | Purpose | Cost/Month |
|---|---|---|
| DigitalOcean Spaces | Paintings, assets | $5-20 |
| MongoDB Atlas | All data | $0-50 |
| Auth0 | Authentication | $25-240 |
| Total | $30-310 |
After ATProto Integration #
| Service | Purpose | Cost/Month | Notes |
|---|---|---|---|
| DigitalOcean Spaces | Paintings, assets | $5-20 | No change |
| MongoDB Atlas | All AC data | $0-50 | No change |
| GCP Compute (PDS) | Identity & auth | $15-20 | Replaces Auth0 |
| PostgreSQL | ATProto repos | $0-15 | Small dataset |
| PDS Blob Storage | Shared content | $1-10 | Only published items |
| Total | $21-115 |
Net Savings: $9-195/month
Backup & Disaster Recovery #
Backup Strategy #
MongoDB (Primary AC data):
- Daily automated backups (existing)
- No changes to backup strategy
- All AC-specific content protected
DigitalOcean Spaces (Media files):
- S3-compatible backup (existing)
- No changes needed
- All painting files protected
PDS PostgreSQL (Identity data):
- New: Daily PostgreSQL dumps
- Smaller dataset, easier to backup
- Can restore from MongoDB mapping if needed
Disaster Recovery Scenarios:
| Scenario | Impact | Recovery |
|---|---|---|
| PDS server dies | Users can't login | Restore PDS from backup, 15 min downtime |
| MongoDB dies | All AC data lost | Restore from backup, MongoDB replica set |
| DO Spaces dies | Paintings unavailable | Restore from backup, CDN cache helps |
| PDS + MongoDB die | Total failure | Restore both from backups, users re-auth |
Key insight: If PDS fails but MongoDB survives, you still have all AC data and can rebuild PDS from MongoDB user mappings.
Decision Tree: Where Does Data Go? #
Is this user identity/authentication?
├─ Yes → PDS (PostgreSQL)
└─ No → Is this ATProto-standard content (posts, follows)?
├─ Yes, AND user chose to publish → PDS (PostgreSQL)
└─ No → Is this AC-specific content?
├─ Yes → MongoDB
└─ Is this a media file?
├─ Yes → DigitalOcean Spaces
└─ Yes, AND shared to ATProto → Also copy to PDS blobs
Summary #
The Golden Rule:
PDS handles identity & federation.
MongoDB handles everything else.
DigitalOcean handles all media.
Publishing to ATProto creates copies, never moves data.
Migration Impact:
- Minimal: Only user ID references change in MongoDB
- No data transfer needed for paintings/moods/kidlisp
- All files stay in DigitalOcean Spaces
- Existing URLs continue to work
Cost Impact:
- Eliminate Auth0 ($25-240/month)
- Add PDS hosting ($15-35/month)
- Net savings: $9-195/month
User Impact:
- Better: ATProto identity works everywhere
- Better: Can share to Bluesky/ATProto network
- Same: All AC features work exactly as before
- Same: All existing content accessible