Painting Short Code System Implementation Plan#
Date: October 9, 2025
Goal: Implement a global short identifier system for paintings similar to KidLisp's $code system, using #code format (hashtag prefix for bitmaps)
Implementation: Single comprehensive commit with all components
Tools: CLI suite in /paintings directory for inspection, migration, and orchestration
CLI Tools & Inspection Suite#
Location: /paintings directory
Available Tools#
inspect-spaces.mjs- Browse Digital Ocean Spaces, list paintings, check sizesinspect-mongodb.mjs- Query MongoDB, check for codes, get statisticsinspect-api.mjs- Test API endpoints (local or live server)
Quick Start#
cd paintings
./setup.fish # Install dependencies and setup .env
npm run inspect:mongodb -- --stats
npm run inspect:spaces -- --list
npm run inspect:api -- --tv
Environment Configuration#
Tools support both local development and live production:
# Local development
AC_API=http://localhost:8888
MONGODB_URI=mongodb://localhost:27017/aesthetic
# Live production
AC_API=https://aesthetic.computer
MONGODB_URI=mongodb+srv://...
See /paintings/README.md for full documentation.
Current State Analysis#
KidLisp Code System ($code)#
Location: /system/public/aesthetic.computer/disks/store-kidlisp.mjs
What Works:
- ✅ Short, memorable 3-12 character codes (e.g.,
$beli,$waf) - ✅ Smart inference from source content (function names, meaningful combinations)
- ✅ Phonetically-balanced generation (vowel-consonant patterns)
- ✅ Collision detection via SHA-256 hash
- ✅ Automatic deduplication (same source = same code)
- ✅ MongoDB collection with indexes:
code(unique),hash(unique),when,user - ✅ nanoid-based generation with custom alphabets
- ✅ Progressive length growth (3→12 chars) if collisions occur
- ✅ Hit tracking and analytics
Database Structure:
// kidlisp collection
{
code: "beli", // 3-12 char unique identifier
source: "(wipe 'blue')...", // Original KidLisp source
hash: "sha256hash", // For deduplication
user: "auth0|...", // Optional user attribution
when: Date, // Creation timestamp
hits: 42, // Usage counter
lastAccessed: Date // Analytics
}
Painting Upload/Creation Flow (Current)#
Step 1: Client-Side Upload Initiation
- Location:
/system/public/aesthetic.computer/disks/prompt.mjs(lines 987-1030) - User paints, types "done" or "upload"
- System generates filename:
painting-${num.timestamp()}.png - Calls
upload()function with PNG pixel data
Step 2: Presigned URL Generation
- Endpoint:
/system/netlify/functions/presigned-url.js - Client requests:
GET /presigned-upload-url/png/painting-{slug}/user - Server logic:
- Checks user authentication via
authorize(event.headers) - Uses nanoid (8 chars, alphabet:
0-9A-Za-z) for collision prevention - S3 bucket selection:
artbucket: Guest uploads (temporary, expiring)userbucket: Authenticated uploads (permanent, user/{auth0_id}/ prefix)
- Returns presigned S3 URL valid for 1 hour (3600s)
- Checks user authentication via
Step 3: S3 Upload
- Client uploads PNG directly to Digital Ocean Spaces via presigned URL
- PUT request with
image/pngmime type,public-readACL - Storage path:
{user_id}/painting/{slug}.png - File becomes accessible at CDN URL
Step 4: Database Record Creation
- Endpoint:
/system/netlify/functions/track-media.js(POST handler) - Location: Lines 38-57
- Client sends:
{ slug, ext: "png" } - Server creates MongoDB record:
{ slug: "2025.10.09.09.51.18.882", // Timestamp from upload user: "auth0|...", // From JWT token when: new Date() // Server timestamp } - Indexes created:
user,when,slug,slug+user(unique)
Step 5: Navigation
- Client jumps to:
painting~@handle/{slug}orpainting~{slug} - Painting viewer loads PNG from Digital Ocean CDN
Existing Admin/Migration Tools#
Migration Script: /system/scripts/admin-migrate.mjs
# Usage
node scripts/admin-migrate.mjs painting # Migrate paintings
node scripts/admin-migrate.mjs piece # Migrate pieces
Migration Function: /system/backend/database.mjs (listAndSaveMedia())
- Scans Digital Ocean Spaces buckets for orphaned files
- Iterates through all
auth0|*/painting/*.pngfiles - Creates missing database records with
{slug, user, when} - Logs:
✅ Added painting entry for: {slug}or⚠️ painting already exists
Prompt Command: admin:migrate-painting or admin:migrate-piece
- Location:
/system/public/aesthetic.computer/disks/prompt.mjs(lines 1312-1318) - Calls:
/api/admin?migrate=painting
Painting System (Current Database)#
Collection: paintings in MongoDB
Existing Schema:
{
slug: "2025.10.09.09.51.18.882", // Timestamp-based (REQUIRED)
user: "auth0|...", // Auth0 user ID
when: Date, // Creation timestamp
nuked: false // Soft delete flag (optional)
}
Indexes:
user(ascending)when(ascending)slug(ascending)slug + user(unique composite)
Known Issues:
- ❌ NO short codes currently
- ⚠️ Some paintings may not be tracked in MongoDB (orphans in S3)
- ❌ No metadata (dimensions, colors, etc.)
- ❌ No deduplication mechanism
- ❌ No hit tracking/analytics
Current URL Patterns:
Long: https://aesthetic.computer/painting~@handle/2025.10.09.09.51.18.882
Short: (doesn't exist yet)
Goal: https://aesthetic.computer/#waf or aesthetic.computer/painting~#waf
Proposed Solution#
1. Hashtag Prefix Convention#
- Format:
#waf,#lor,#pix(3-4 characters preferred) - Prefix:
#for bitmaps (paintings),$for code (KidLisp) - Benefits:
- Clear semantic distinction (# = bitmap, $ = code)
- Short, memorable, typeable
- Works in URLs (# gets encoded as %23 or used as fragment)
- Social media friendly
2. Database Schema Enhancement#
Add to existing paintings collection:
{
// Existing fields
slug: "2025.10.09.09.51.18.882",
user: "auth0|...",
when: Date,
nuked: false,
// NEW fields
code: "waf", // Short unique identifier (3-12 chars)
hash: "sha256...", // Hash of pixel data for deduplication
hits: 0, // Usage tracking
lastAccessed: Date, // Analytics
metadata: { // Optional enrichment
width: 195,
height: 372,
colors: 8, // Palette size
tags: ["pixel-art"], // Auto-generated or manual
title: "Sunset Scene" // Optional user title
}
}
New Indexes Needed:
- { code: 1 } - unique, for #waf lookups
- { hash: 1 } - unique, for deduplication
- { user: 1, code: 1 } - for user galleries by code
3. Code Generation Strategy#
Option A: Smart Inference (Like KidLisp)
- Extract visual features from painting
- Generate codes based on:
- Dominant colors ("red" →
#rad,#rox) - Dimensions ("16x16" →
#pix,#dot) - User handle initials (@fifi →
#fif,#fie) - Random pronounceable patterns
- Dominant colors ("red" →
Option B: Pure Random (Simpler)
- Use nanoid with vowel-consonant balanced alphabet
- Start at 3 chars, grow to 4, 5 if collisions
- Alphabet:
abcdefghijklmnopqrstuvwxyz0123456789 - Prefer CVC patterns (consonant-vowel-consonant)
Option C: Hybrid (Recommended)
- Try smart inference first (5-10 attempts)
- Fall back to random generation
- Ensure pronounceability with vowel injection
Implementation Components#
Approach: Single-commit implementation with all components integrated into existing upload flow
Component 1: Code Generator Module#
File: system/backend/painting-code-generator.mjs
Purpose: Reusable module for generating short codes for paintings
Implementation:
- Standalone pure function:
generatePaintingCode(imageBuffer, user, existingCodes) - Smart inference: Extract visual features (colors, dimensions, user handle)
- Fallback: Random pronounceable codes using nanoid with CVC patterns
- Collision detection: Check MongoDB before returning
- Hash generation: SHA-256 of pixel data for deduplication
- Progressive length: Start at 3 chars, grow to 4, 5, etc. if collisions
Dependencies:
- nanoid (for random generation)
- sharp (for image analysis)
- crypto (for SHA-256 hashing)
Testing:
- Unit tests with sample images
- Collision rate testing (should be < 0.01%)
- Pronounceability validation
Component 2: Upload Flow Integration#
File: system/netlify/functions/track-media.js (POST handler)
Current Flow:
- Client uploads PNG to S3 via presigned URL
- Client POSTs
{ slug, ext }to track-media - Server creates MongoDB record:
{ slug, user, when }
Enhanced Flow:
- Client uploads PNG to S3 via presigned URL
- [NEW] Server downloads PNG from S3 (or receives hash from client)
- [NEW] Server generates code using
painting-code-generator.mjs - [NEW] Server calculates SHA-256 hash of image data
- Server creates MongoDB record:
{ slug, user, when, code, hash, hits: 0 } - [NEW] Server returns
{ slug, code }to client - [NEW] Client can navigate to
painting~#codeorpainting~@handle/slug
Implementation Details:
- Import code generator module
- Add collision retry logic (max 10 attempts)
- Handle hash-based deduplication (same image = reuse code)
- Update indexes: add
code(unique),hash(unique) - Maintain backward compatibility (slug-only still works)
Component 3: Migration Script#
File: system/scripts/migrate-paintings-add-codes.mjs
Purpose: Add codes and hashes to existing paintings
Implementation:
# Dry run (preview only)
node scripts/migrate-paintings-add-codes.mjs --dry-run
# Execute migration
node scripts/migrate-paintings-add-codes.mjs --execute
# Migrate specific user
node scripts/migrate-paintings-add-codes.mjs --user auth0|123 --execute
Algorithm:
- Query MongoDB for paintings without
codefield - For each painting:
- Download PNG from Digital Ocean CDN
- Generate code using
painting-code-generator.mjs - Calculate SHA-256 hash
- Check for hash duplicates (deduplication)
- Update document with
{ code, hash, hits: 0, lastAccessed: Date }
- Log progress:
✅ slug → #codeor⚠️ Duplicate hash, reusing #code - Summary: Total migrated, duplicates found, failures
Error Handling:
- Skip paintings that fail to download (404, network error)
- Retry up to 3 times on collision
- Log failures to separate file for manual review
- Rate limit: 100ms delay between requests to avoid S3 throttling
Reuse Admin Infrastructure:
- Extend existing
admin-migrate.mjswith--add-codesflag - Use same S3 client and MongoDB connection
- Add to prompt commands:
admin:migrate-painting-codes
Component 4: Lookup API#
File: system/netlify/functions/painting-code-lookup.mjs
Endpoint: GET /api/painting-code/{code}
Input:
codeparameter:wafor#waf(strip # if present)
Output:
{
"code": "waf",
"slug": "2025.10.09.09.51.18.882",
"user": "auth0|...",
"handle": "fifi",
"when": "2025-10-09T09:51:18.882Z",
"url": "https://aesthetic.computer/painting~@fifi/2025.10.09.09.51.18.882",
"shortUrl": "https://aesthetic.computer/#waf",
"cdnUrl": "https://aesthetic.computer/media/@fifi/painting/2025.10.09.09.51.18.882.png"
}
Features:
- Strip
#prefix if present - Case-insensitive lookup (convert to lowercase)
- Increment
hitscounter - Update
lastAccessedtimestamp - Return 404 if code not found
- Include user handle via join/lookup
Authorization: Public endpoint (no auth required)
Component 5: URL Routing#
File: system/public/aesthetic.computer/disks/prompt.mjs
Current Routes:
painting~@handle/slug→ User's specific paintingpainting~slug→ Painting by current user
New Routes:
painting~#waf→ Painting by code (any user)#waf→ Direct shortcut to painting by code
Implementation:
- Update URL parser to detect
#codepattern - Call
/api/painting-code/wafto resolve code → slug/user - Load painting with resolved slug
- Display code prominently in UI
- Generate QR code for
aesthetic.computer/#waf
Fragment Routing:
- Handle
/#wafat top level - Check if fragment matches
#[a-z0-9]{3,12}pattern - If match, treat as painting code and route to
painting~#waf - Otherwise, handle as normal prompt command
Component 6: TV API Enhancement#
File: system/netlify/functions/tv.mjs
Current Response:
{
"media": {
"paintings": [
{ "slug": "2025...", "user": "auth0|...", "owner": { "handle": "@fifi" }, ... }
]
}
}
Enhanced Response:
{
"media": {
"paintings": [
{
"slug": "2025...",
"code": "waf",
"shortUrl": "https://aesthetic.computer/#waf",
"user": "auth0|...",
"owner": { "handle": "@fifi" },
...
}
]
}
}
Implementation:
- Add
codefield to projection - Generate
shortUrlin response - Filter by code:
GET /api/tv?code=waf - Order by hits:
GET /api/tv?sort=popular
Component 7: Orphan Discovery#
File: system/scripts/audit-digital-ocean-paintings.mjs
Purpose: Find paintings in S3 that aren't tracked in MongoDB
Implementation:
- List all files in Digital Ocean Spaces:
auth0|*/painting/*.png - Query MongoDB for all painting slugs
- Compare lists to find orphans
- Generate report:
orphaned-paintings.json - Optionally auto-import with generated codes
Integration:
- Reuse
listAndSaveMedia()fromdatabase.mjs - Extend with code generation
- Add
--importflag to create records for orphans
Component 8: Frontend Display#
File: system/public/aesthetic.computer/disks/painting.mjs
Updates:
- Display Code: Show
#wafprominently on painting page - Copy Button: Click to copy short URL to clipboard
- QR Code: Generate QR for
aesthetic.computer/#waf - Share Menu: Update to use short URL by default
- Legacy Support: Still show timestamp slug as fallback
UI Mockup:
┌─────────────────────────┐
│ 🎨 Painting #waf │
│ by @fifi │
│ │
│ [📋 Copy Link] │
│ [🔗 QR Code] │
│ [↗️ Share] │
│ │
│ aesthetic.computer/#waf
└─────────────────────────┘
Technical Details#
Code Generation Algorithm#
async function generatePaintingCode(imageBuffer, user, existingCodes = new Set()) {
const { customAlphabet } = await import('nanoid');
const lowercaseAlphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
// Try smart inference first
const inferredCodes = await inferCodesFromImage(imageBuffer, user);
for (const code of inferredCodes) {
if (!existingCodes.has(code) && !await codeExists(code)) {
return code;
}
}
// Fall back to random CVC pattern generation
const generator = customAlphabet(lowercaseAlphabet, 3);
let attempts = 0;
let code;
do {
code = generator();
// Ensure pronounceability (consonant-vowel-consonant pattern)
if (!/[aeiou]/.test(code)) {
code = injectVowel(code);
}
attempts++;
} while (existingCodes.has(code) || await codeExists(code));
return code;
}
async function inferCodesFromImage(imageBuffer, user) {
const codes = [];
const metadata = await sharp(imageBuffer).metadata();
const { width, height } = metadata;
// Size-based codes
if (width <= 16 || height <= 16) codes.push('pix', 'dot', 'sml');
if (width >= 1024 || height >= 1024) codes.push('big', 'hiq');
// Aspect ratio codes
if (Math.abs(width - height) < 10) codes.push('sqr');
if (height > width * 1.5) codes.push('tal', 'ver');
if (width > height * 1.5) codes.push('wid', 'hor');
// User-based codes
if (user && user.handle) {
const handle = user.handle.replace('@', '');
codes.push(
handle.substring(0, 3),
handle.charAt(0) + handle.charAt(1) + handle.charAt(handle.length - 1)
);
}
// Make all codes pronounceable
return codes.map(c => ensurePronounceable(c)).filter(c => c.length >= 3);
}
Migration Script Structure#
// migrate-paintings-add-codes.mjs
import { connect } from '../backend/database.mjs';
import { generatePaintingCode } from '../backend/painting-code-generator.mjs';
import fetch from 'node-fetch';
async function migratePaintings(dryRun = true) {
const { db, disconnect } = await connect();
const paintings = db.collection('paintings');
// Find paintings without codes
const query = { code: { $exists: false }, nuked: { $ne: true } };
const toMigrate = await paintings.find(query).toArray();
console.log(`Found ${toMigrate.length} paintings to migrate`);
const existingCodes = new Set(
(await paintings.find({ code: { $exists: true } }).toArray())
.map(p => p.code)
);
let migrated = 0;
let failed = 0;
for (const painting of toMigrate) {
try {
// Download painting
const url = `https://aesthetic.computer/media/${painting.user}/painting/${painting.slug}.png`;
const response = await fetch(url);
const imageBuffer = Buffer.from(await response.arrayBuffer());
// Generate code
const code = await generatePaintingCode(imageBuffer, { user: painting.user }, existingCodes);
const hash = crypto.createHash('sha256').update(imageBuffer).digest('hex');
if (!dryRun) {
await paintings.updateOne(
{ _id: painting._id },
{ $set: { code, hash, hits: 0, lastAccessed: new Date() } }
);
}
console.log(`✅ ${painting.slug} → #${code}`);
existingCodes.add(code);
migrated++;
// Rate limit
await sleep(100);
} catch (error) {
console.error(`❌ Failed: ${painting.slug}`, error.message);
failed++;
}
}
console.log(`\n✨ Migration complete: ${migrated} migrated, ${failed} failed`);
await disconnect();
}
// Run with: node migrate-paintings-add-codes.mjs --dry-run
// Or: node migrate-paintings-add-codes.mjs --execute
URL Structure Comparison#
Current System#
View: aesthetic.computer/painting~@fifi/2025.10.09.09.51.18.882
Share: (same - very long URL)
QR: (impractical - too long)
New System#
View: aesthetic.computer/#waf (fragment-based routing)
Or: aesthetic.computer/painting~#waf
Legacy: aesthetic.computer/painting~@fifi/2025.10.09.09.51.18.882 (still works)
QR: aesthetic.computer/#waf (perfect for QR codes!)
Social: @aesthetic.computer just posted #waf 🎨
Benefits#
- Short URLs - Perfect for QR codes, social media, typing
- Memorable -
#wafis easier to remember than a timestamp - Consistent - Matches KidLisp's
$codepattern - Semantic -
#clearly denotes bitmap/image content - Deduplication - Same painting = same code (via hash)
- Analytics - Track painting popularity via hits
- Discovery - Enable "random painting" via random code
- Social - Shareable hashtags (#waf) that work as URLs
Risks & Mitigation#
Risk 1: Code Collisions#
Mitigation:
- Unique index on
codefield - Progressive length growth (3→12 chars)
- 36^3 = 46,656 combinations at 3 chars
- 36^4 = 1,679,616 combinations at 4 chars
- Check for collisions before inserting
Risk 2: Untracked Paintings in DO Spaces#
Mitigation:
- Phase 3 audit script finds orphans
- Bulk import with generated codes
- Gradual migration (non-blocking)
Risk 3: Hash Collisions#
Mitigation:
- SHA-256 is cryptographically secure
- Paranoid verification: compare actual pixels on collision
- Fall back to separate code if true collision
Risk 4: Existing URLs Breaking#
Mitigation:
- Maintain backward compatibility
- Keep timestamp-based slugs working
- Add codes as alternative access method
- No existing URLs change
Risk 5: Code Generation Bias#
Mitigation:
- Test smart inference with diverse paintings
- Monitor code distribution
- Fall back to random if inference fails
- Allow manual code assignment for special cases
Success Metrics#
- Code Coverage: 100% of paintings have unique codes within 2 weeks
- Collision Rate: < 0.01% during generation
- Pronounceability: > 80% of codes pass phonetic test
- Adoption: Short URLs used in 50%+ of shares within 1 month
- Performance: Code lookup < 50ms (with indexes)
- QR Usability: 90%+ of QR codes successfully scan and resolve
Next Steps#
- Review this plan with team
- Prioritize phases based on urgency
- Prototype code generator in
/atdirectory first - Test on subset of paintings (100-1000)
- Full migration once validated
- Update docs and announce new feature
Open Questions#
- Should codes be case-sensitive? (Recommend: no, lowercase only)
- Allow users to request custom codes? (vanity codes like #jeffrey)
- Reserve certain codes? (e.g., #test, #admin, #new)
- Integrate with existing "nuke" functionality?
- Should codes be transferable between paintings? (Recommend: no)
- Create separate namespace for featured/curated? (e.g., #featured:waf)
References#
Existing Systems:
- KidLisp:
/system/netlify/functions/store-kidlisp.mjs - Paintings:
/system/netlify/functions/track-media.js - Database:
/system/backend/database.mjs
Similar Services:
- Imgur: 7-char alphanumeric (e.g.,
a1b2c3d) - TinyURL: Variable length short codes
- Bitly: Custom short links
- Instagram: 11-char base64 post IDs
Status: 📋 Planning - Ready for Review
Next Action: Prototype code generator in /at directory