CRUMB a card from devarno-cloud

Core Data Models

smo1 intermediate 8 min read

ELI5

SMO1 stores four main things: who you are (User), what links you made (Link), badges you unlocked (Achievement), and secret passwords for apps (API Key). Each one has a specific shape — like a form with required and optional fields — and they connect to each other like a family tree.

Technical Deep Dive

Entity Relationship

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#e8f4f8', 'primaryTextColor': '#2d3748', 'primaryBorderColor': '#90cdf4', 'lineColor': '#718096', 'secondaryColor': '#f0fff4', 'tertiaryColor': '#fefcbf'}}}%%
erDiagram
USER ||--o{ LINK : creates
USER ||--o{ API_KEY : owns
USER ||--o{ ACHIEVEMENT : earns
USER ||--o{ XP_EVENT : generates
LINK ||--o{ LINK_CLICK : receives
USER {
text id PK
text email
text username
text display_name
text avatar_url
enum subscription_tier
text polar_customer_id
text polar_subscription_id
enum subscription_status
timestamptz subscription_period_end
boolean subscription_cancel
bigint xp
int level
int streak_current
int streak_longest
timestamptz last_streak_at
bigint total_clicks
int total_links
int monthly_links_used
timestamptz created_at
timestamptz updated_at
timestamptz last_login_at
}
LINK {
uuid id PK
text user_id FK
text original_url
text uid
text custom_slug
text display_name
bigint total_clicks
bigint unique_visitors
timestamptz last_clicked_at
text utm_source
text utm_medium
text utm_campaign
text utm_term
text utm_content
enum protection_type
text protection_hash
text protection_hint
int protection_max_attempts
enum content_classification
enum redirect_mode
boolean is_active
timestamptz expires_at
timestamptz created_at
timestamptz updated_at
}
ACHIEVEMENT {
uuid id PK
text name
text description
enum category
jsonb criteria
text icon_url
int xp_reward
timestamptz created_at
}
API_KEY {
uuid id PK
text user_id FK
text key_hash
text key_prefix
text[] permissions
timestamptz expires_at
timestamptz revoked_at
timestamptz last_used_at
timestamptz created_at
}

User Model

The User record is created automatically on first Airlock JWT login. The id is a text field (not UUID) because it comes from the Airlock identity provider.

Key groups:

  • Identity: id, email, username, display_name, avatar_url
  • Subscription (Polar): subscription_tier (free/pro/business/early_adopter), polar_customer_id, polar_subscription_id, subscription_status, subscription_period_end, subscription_cancel
  • Gamification: xp, level, streak_current, streak_longest, last_streak_at
  • Stats: total_clicks, total_links, monthly_links_used

The central entity. Two identifiers coexist:

  • uid — 6-character auto-generated slug ([a-zA-Z0-9]), cryptographically random
  • custom_slug — optional user-defined vanity slug (e.g., my-event)

Protection fields:

  • protection_type: none | password | pin
  • protection_hash: bcrypt hash (never exposed in API; json:"-" tag)
  • protection_hint: user-facing hint (e.g., “My dog’s name”)
  • protection_max_attempts: 5 (enforced per IP per link)

Lifecycle:

  • is_active: soft-delete flag
  • expires_at: optional TTL
  • redirect_mode: redirect (302) or proxy (reverse proxy)

LinkDTO (API Response)

The API never returns raw Link structs to clients. It returns LinkDTO with computed fields:

FieldSourceDescription
short_urluid or custom_slugFully qualified short URL
clicks_7dClickHouse aggregationClicks in the last 7 days
statusAlgorithmCat-themed activity label (see smo1-012)
pawprintzAlgorithm0–100 activity score
treatzAlgorithm0–100 compression efficiency
niblzAlgorithmRaw characters saved
qr_code_urlMinIO/S3Pre-generated QR code image URL

API Key

  • Prefix: sk_ (Stripe-style convention)
  • Storage: Only the SHA-256 hash is stored; the plaintext is shown once at creation
  • Identification: key_prefix stores the first 8 characters so the user can tell keys apart in the UI
  • Revocation: revoked_at timestamp; revoked keys are filtered out at lookup time

Achievement

  • Categories: getting_started, link_master, analytics_pro, engagement_king, streak_champion, secret
  • Criteria: JSONB blob with unlock conditions (e.g., { "min_links": 10, "min_clicks": 100 })
  • Progress tracking: UserAchievement junction table tracks current_value and unlocked_at

Key Terms

  • LinkDTO → API-facing data transfer object with computed scores and URLs
  • uid → 6-character cryptographically random alphanumeric slug
  • protection_hash → bcrypt hash of password/PIN; never exposed via API
  • key_prefix → First 8 characters of an API key plaintext, stored for UI identification
  • content_classificationgeneral, personal, work, or nsfw — used for safety filtering
  • Polar → Payment provider; stores customer and subscription IDs on the User record

Q&A

Q: Why is the user ID text instead of UUID? A: It is sourced from the Airlock OIDC provider. Using the provider’s subject identifier avoids identity fragmentation across systems.

Q: Can a link have both a uid and a custom_slug? A: Yes. Both exist simultaneously. The short URL uses custom_slug if present, otherwise falls back to uid.

Q: Why store protection_hash with json:"-"? A: This Go struct tag prevents the field from ever being serialized to JSON, ensuring the bcrypt hash never leaks through the API.

Q: How many API keys can a user have? A: There is no hard limit enforced in the schema, but the UI and service layer may impose reasonable caps per tier.

Examples

Think of a User as a gym membership card:

  • Identity fields are your name and photo on the card
  • Subscription fields are your membership tier (basic/premium) and renewal date
  • Gamification fields are your loyalty points, visit streak, and current level
  • Stats fields are the total number of times you checked in and how many guest passes you used this month

A Link is like a numbered locker:

  • uid is the randomly assigned locker number
  • custom_slug is a nickname you write on a sticky note
  • original_url is what is actually inside the locker
  • protection_type is whether the locker has a keypad, a password lock, or nothing
  • redirect_mode is whether you tell people “go look in locker 42” (redirect) or you hand them the contents through a window (proxy)

neighbors on the map