Skip to content

Data Contracts

github-actions[bot] edited this page Aug 10, 2026 · 50 revisions

Data Contracts

For how account restrictions, posting IP bans, hard bans, and browser/IP propagation interact, see Restrictions and Moderation.

This page documents live data shapes. The separate Developer Data page is the single source of truth for which fields and files are sanitized or excluded from public development copies.

Big Rule

Runtime data lives under /data.

Repo intent:

  • /data is not supposed to be versioned normally
  • .gitignore ignores /data/**/*
  • .rsyncignore excludes /data/** from deployment sync

So local/dev/prod data has to be managed separately.

data/accounts/

accounts.json

Expected top-level shape:

{
  "accounts": [
    {
      "username": "string",
      "name": "string",
      "password": "bcrypt-hash or empty",
      "isAdmin": true,
      "postingRestricted": false,
      "mustResetPassword": false,
      "discordUserId": "optional discord snowflake string",
      "emailAddress": "optional @fridge.dev email address",
      "allowedPages": ["feed", "journal", "comments", "chat"],
      "bookmarks": ["2026-01-01_12-00-00", "journal:12"],
      "theme": "default|classic|theme-id",
      "glowIntensity": "none|medium",
      "onekoEnabled": true,
      "reduceMotion": false,
      "titleAnimation": "wobble",
      "titleAnimationAlways": false,
      "titleAnimationDesync": true,
      "colors": {
        "bg": "#RRGGBB",
        "fg": "#RRGGBB",
        "border": "#RRGGBB",
        "subtle": "#RRGGBB",
        "links": "#RRGGBB"
      }
    }
  ]
}

Notes:

  • Extra unknown keys can exist and are preserved by account/admin/edit
  • Bookmarks are the current source of truth for logged-in users
  • Bookmark ids currently use raw feed ids and journal:{id}; legacy newsletter:{id} values can exist but are ignored
  • theme: default is Blackprint and uses the base template plus /style.css; theme: classic enables saved colors for bg/fg/border/subtle/links; theme: ambercrt is shown as CRT and uses only saved colors.links as its main phosphor color; any other valid value refers to a /themes/{theme-id}.json file with name, description, thumbnail, html, and css
  • Legacy blackprint normalizes to default, custom normalizes to classic, newsprint normalizes to whiteprint, crt normalizes to ambercrt, and removed liminal/syswave preferences normalize to default
  • Text glow is stored in glowIntensity; the settings UI writes none for off and medium for on, while legacy low/high values are treated as enabled medium glow when saved again
  • Title motion is stored in titleAnimation (wobble, bounce, rubberhose, bubble, slot-machine, moonwalk, or heartbeat), titleAnimationAlways (boolean), and titleAnimationDesync (boolean, default true); removed pinball values migrate to wobble; legacy orbit, domino, and lava-lamp values migrate to bubble; removed tidal-wave, accordion, and typewriter values migrate to slot-machine, while helicopter, haunted, and juggle migrate to moonwalk; guests keep the same values in local storage
  • Accessibility toggles are stored as account booleans such as reduceMotion; logged-out browsers keep the same preferences in localStorage
  • Legacy browserNotificationsEnabled and journalBrowserNotificationsEnabled keys may remain in older account records as unknown preserved fields, but the application no longer reads, writes, or exposes them and does not use the browser Notification API
  • mustResetPassword is used by the shared session bootstrap to force first-login password changes
  • postingRestricted is an admin-managed account boolean; when enabled, server handlers reject new or edited feed posts, journal posts/drafts, feed replies, chat conversations/messages, guestbook entries, contact submissions, mdpaste creation, and upload room/signaling use, while matching composer notices keep text fields, formatting controls, uploads, and submit controls disabled
  • discordUserId links a site account to a Discord member for bot DMs and notifications
  • emailAddress marks accounts with a fridge.dev email mailbox; when present and valid, shared chrome swaps the footer Discord button to /account/email, and /account/email shows the assigned address
  • allowedPages currently includes functional grants like feed, journal, comments, and chat

data/chat/

One-time private conversations live as individual encrypted JSON envelopes:

{
  "version": 1,
  "cipher": "aes-256-gcm",
  "nonce": "base64",
  "tag": "base64",
  "ciphertext": "base64"
}

Notes:

  • Each file is named {conversationId}.json; new chat ids are 9 lowercase letters/numbers
  • Legacy 32-character lowercase hex ids are still accepted so older active links do not break
  • Decrypted payloads contain conversation metadata, the recipient label in name, the recipient cookie hash, and message records
  • Messages may include an attachment object with encrypted blob metadata: id, name, mime, and size; image/audio/video attachments are served inline through the authorized chat route so they can render or play in the chat UI
  • Messages may include replyTo with another message id, plus reactions keyed by valid emoji sequences with active viewer roles such as manager or participant
  • Conversations may include participantUsername when a logged-in account claims the invite, or participantHash when an anonymous browser cookie claims it; the first-open popup copy changes based on that claim type
  • Conversations may include recipientIntroSeenAt once the recipient has seen the first-open security/help popup
  • Recipient cookies are HttpOnly and scoped to /chat
  • The first non-manager account or anonymous browser to open /chat/{conversationId} claims the recipient slot
  • Account-linked recipients can delete their own active chat; anonymous cookie-linked recipients cannot
  • Admins and accounts with allowedPages containing chat can create, view, and delete conversations without claiming the recipient slot
  • Deleting a conversation unlinks the encrypted JSON file immediately
  • Encryption uses FRIDG3_CHAT_KEY when set; otherwise the app creates data/chat/.chat_key
  • Lightweight presence indicators use sidecar files under data/chat/.presence/{conversationId}.json; current entries store lastSeen, active, and a short-lived typingUntil, while older timestamp-only entries are still readable
  • Attachments are encrypted AES-256-GCM envelopes under data/chat/.attachments/{conversationId}/; they are served only through the authorized chat route and are deleted with the conversation
  • Attachment uploads are capped at 8 MB

/themes/

Theme metadata lives as JSON files directly under /themes.

{
  "name": "Theme Name",
  "html": "template-file.html",
  "css": "stylesheet-file.css"
}

Notes:

  • The metadata filename is the saved theme id, for example /themes/cool.json becomes cool
  • name is the label shown in /settings
  • description is the short supporting text shown under the theme name in the picker
  • thumbnail is a 4:3 preview path relative to /themes, usually thumbnails/{theme-id}.svg
  • html and css must be relative paths in /themes/lib, for example aero/aero.html and aero/aero.css
  • Theme asset paths cannot be absolute, contain .., or use characters outside letters, numbers, ., _, -, and /
  • Desktop rendering uses both themed HTML and CSS
  • Mobile rendering keeps template_mobile.html and only swaps the CSS

login_attempts.json

  • Map of client IP -> unix timestamp array
  • Used for login throttling

data/feed/

Legacy feed post format:

  1. @username
  2. YYYY-MM-DD HH:MM:SS
  3. Body text / BBCode

Version 2 feed post format:

  1. v2
  2. @username
  3. YYYY-MM-DD HH:MM:SS
  4. Feed-subset Markdown body

Readers must inspect the first line before assigning username/date offsets. Files without the exact v2 marker remain legacy BBCode and must not be passed through the Markdown renderer.

The v2 feed subset supports **bold**, *italic*, <u>underline</u>, ~~strikethrough~~, ==highlight==, [links](URL), feed media syntax, > blockquotes, inline/fenced code, pipe tables, ||spoiler text||, nested ordered/unordered lists, and Font Awesome icons using !fa style icon-name. Lists and icons are supported through typed syntax without toolbar buttons. Other Markdown constructs and arbitrary HTML are displayed literally.

Other file:

  • index.toml is generated by /feed/index.php

Feed bodies can include public voice notes as BBCode:

[audio=/data/audio/voice/example.m4a][name:voice-note.m4a]

Voice notes are created from temporary [voice:N] editor placeholders, verified at upload time, normally transcoded to small mono .m4a files, and stored under data/audio/voice/. If ffmpeg cannot decode a valid browser recording container, the validated original WebM, Ogg, MP4/M4A, MP3, or WAV recording is retained so posting does not fail solely because of browser codec differences.

data/feed/replies/

Per-post replies live in {postId}.json files shaped roughly like:

{
  "replies": [
    {
      "id": "20260413153000_deadbeef",
      "username": "Anonymous",
      "date": "2026-04-13 15:30:00",
      "body": "reply body with Markdown",
      "format": "v2",
      "parentId": "optional parent reply id for comment replies",
      "isGuest": true,
      "ip": "203.0.113.10",
      "guestBrowserId": "optional browser-local in-site inbox identity"
    }
  ]
}

Notes:

  • Reply ids are generated on write; older data may be normalized into legacy_* ids at read time
  • New account and guest replies store format: "v2" and use the restricted feed Markdown renderer. For compatibility with stale cached writers, missing markers fall back to the rollout timestamp and then body inspection: recognized legacy BBCode stays legacy, while plain or Markdown bodies use the restricted Markdown renderer; plain older replies remain visually unchanged
  • Replies to individual comments are stored in the same flat array with optional parentId; older top-level replies simply omit it
  • Guest replies may include guestBrowserId, a random browser-local identity used only so guests can receive in-site inbox notifications when someone replies to their comments from another browser/account
  • V2 reply bodies store uploaded images as Markdown and uploaded audio/video or voice notes as the renderer's supported safe media HTML; legacy replies retain their existing media BBCode
  • Guest replies include isGuest: true plus a plaintext ip; guest display names are stored in username, default to Anonymous, cannot match a registered account username case-insensitively, and are filtered with guest reply bodies through /feed/filters/*.txt before storage; matching body text becomes tooltip-wrapped text explaining this phrase was automatically filtered.; guest replies that are mostly filter-list terms are rejected, and guest replies containing filtered text are locked from later guest edits; admin moderation can purge all guest replies with a matching IP without changing the IP ban list
  • Toast-authored reply storage and automatic reply behavior are documented on Toast

data/feed/banned_ips.json

IP-keyed posting-ban records can contain ban metadata and usernames observed for each address. Enforcement and moderation behavior are documented on Restrictions and Moderation.

data/etc/hard-banned-ips.txt

Whitespace-separated exact IPv4 or IPv6 addresses, normalized to one address per line by the admin editor. Enforcement and access behavior are documented on Restrictions and Moderation.

data/etc/hard-ban-identities.json

Private records containing a browser identifier's original manually banned IP, observed IPs, first/last-seen timestamps, and user-agent hash. Propagation and removal behavior are documented on Restrictions and Moderation.

data/etc/hard-ban-settings.json

Global object containing strictIdentityEnforcement and enforcementEnabled booleans, both defaulting to true, plus the exact-address whitelistedIps array. Policy behavior is documented on Restrictions and Moderation, and its access-log controls are documented on Debug Mode.

data/etc/site-notices.json

Global visitor notices managed through the admin-only /settings/notices page. It has independent users and guests blocks, each with an optional banner and popup, plus a pages array for exact-path notices. Page records include path, an audiences array containing users, guests, or both, and type alongside the corresponding banner or popup fields; legacy page records with one string audience are normalized into the array. A matching page notice overrides the global notice of the same type for each selected audience on that path. Banner records contain a revision id, plaintext message, and dismissible flag. Popup records contain a revision id, plaintext title and message, plus optional buttonLabel and site-relative buttonUrl. A new save receives a new revision ID, so browser-local banner dismissal and popup acknowledgement apply only to that saved revision.

data/etc/targeted-notifications.json

Admin-issued persistent inbox notifications for a registered username, exact IP, all logged-in users, or all guests. Multiple selected users/IPs are stored as individual records. Each record contains an immutable ID, target type/value, title, message, site-relative URL, and creation date. Read and dismissal state remains per inbox identity in notification-inbox-state.json.

data/etc/banlists/**/*.txt

Recursive read-only .txt sources containing whitespace-separated exact IPv4/IPv6 addresses or CIDR ranges. Generated binary index data lives beneath data/etc/banlists/index/. Source validation, index construction, caching, fallback behavior, and enforcement are documented on Restrictions and Moderation.

data/journal/

Legacy .txt journal post:

  1. YYYY-MM-DD
  2. Title
  3. Description
  4. Optional CARD_IMAGE:<url>
  5. Trusted HTML body

New journal posts are {id}.md files:

  1. Exact v2 version marker
  2. YAML front matter containing title, description, date, and optional card_image; journal posts do not set author
  3. Full site-supported Markdown body

The description uses the article subtitle styling without an author prefix, and the card-image metadata is not part of the rendered body. When the card image is absent, the listing falls back to the first Markdown or legacy HTML image. Readers discover both .md and .txt, prefer .md when resolving an ID, and render unversioned .txt bodies with the unchanged trusted-HTML legacy path. The parser accepts the earlier v2 author field as a description fallback so development posts created during the transition remain readable.

Draft format:

  1. USER:<username>
  2. Title
  3. Description
  4. Optional FORMAT:html or FORMAT:markdown
  5. Draft body

Without a format marker, preview treats the body as legacy BBCode. FORMAT:html preserves raw-HTML legacy edits, while FORMAT:markdown uses the full Markdown renderer.

data/guestbook/

Entry format:

  1. Timestamp
  2. Display name
  3. Optional IP:<address> metadata for new entries
  4. Message body (line 3 for legacy or sanitized entries without IP metadata)

Plus:

  • ip_index.json for one-post-per-IP ownership tracking
  • Successful entry creation adds one targeted in-site notification per admin account to data/etc/targeted-notifications.json
  • Nginx blocks direct client access to /data/guestbook and its descendants; entries are exposed only through the PHP guestbook and admin moderation views

data/images/

  • Uploaded images used across feed, journal, and gallery content
  • Expected web path is /data/images/<filename>
  • data/images/thumbnails/ contains regenerable 500×500 JPEG thumbnails used by gallery tiles and local journal card backgrounds, keyed by a hash of the original filename; these are excluded from the top-level gallery listing and removed alongside originals by the gallery delete API

Feed and journal attachment uploads use typed temporary [img:N], [audio:N], or [video:N] editor placeholders. Every source media attachment is capped at 8 MB. V2 feed and journal posts resolve these to Markdown images or safe audio/video elements; legacy writers retain their BBCode records. Images keep the existing 1 MB stored-image limit: files already under the limit retain their validated JPEG, PNG, GIF, or WebP format, while larger images are converted to JPEG with PHP GD or an ffmpeg fallback. Validation uses the detected image MIME rather than trusting the filename extension. Audio is stored beneath data/audio/uploads/; video is stored beneath data/video/. Allowed audio formats are MP3, AAC, M4A, OGG, WAV, FLAC, and WebM; allowed video formats are MP4, WebM, Ogg video, and QuickTime. Because libmagic can report audio-only WebM, MP4/M4A, and Ogg containers as their shared video/application types, validated shared containers declared by the browser as audio are classified as audio; MIME parameters such as WebM codecs are ignored during this comparison. Temporary media indexes are reset when SPA navigation creates a new editor, and content with an unresolved upload placeholder is rejected rather than persisted.

data/music/

Artist folders currently include:

  • frdg3
  • cactile

Album JSON shape:

{
  "album_name": "string",
  "album_caption": "string",
  "album_type": "Album|EP|Single|Remix|...",
  "album_art": "/data/images/example.jpg",
  "album_art_directory": "/data/images/example.jpg",
  "scheduled_at": "2026-07-06T21:30:00+01:00",
  "order": 6,
  "songs": [
    { "name": "Track", "directory": "/data/audio/file.wav" }
  ]
}

album_art_directory is preferred by current code.

The /music/upload admin page writes audio files to data/audio/, cover art to data/images/, and creates release JSON in the selected artist folder. single, remix, and album uploads can store multiple tracks, preserving the submitted row order in songs. Release order is assigned automatically as one higher than the current highest order in that artist folder. Admins can optionally set scheduled_at with a publish date/time; future-dated releases stay hidden from non-admins on /music until that timestamp, while admins can still see them with a scheduled label. Uploaded audio accepts mp3, wav, m4a, ogg, and flac with no app-level size cap; uploaded cover art accepts jpg, png, gif, and webp. The deployed Nginx and PHP-FPM config must also allow unlimited request bodies; local PHP dev servers may need equivalent -d upload_max_filesize=0 -d post_max_size=0 startup flags because they do not always read .user.ini.

data/audio/

  • Track files referenced by music metadata
  • Also used by shared playback features
  • data/audio/voice/ stores public feed voice notes as compressed .m4a files
  • data/audio/uploads/ stores feed and journal audio uploads in the same square player style as voice notes, but without the voice-note playback-speed control; the legacy data/audio/attachments/ path remains recognized for cleanup of older posts

data/video/

  • Stores feed and journal video attachments for the inline, site-styled video player; its controls overlay the video and attachment filenames are not displayed

data/contact/

  • Private contact submissions as {YYYYMMDDHHMMSS}_{random}.json
  • Each submission stores id, createdAt, hashed IP, user agent, name, email, message, notification channel id, and optional notifyError
  • Successful submissions also append one targeted in-site notification per admin account to data/etc/targeted-notifications.json
  • rate_limits.json stores hashed client IP keys mapped to recent submission timestamps for throttling
  • Nginx blocks direct web access to this directory; submissions are only shown through the admin-only /contact?dashboard=1 route

data/mdpaste/

  • Temporary markdown paste records as {id}.json
  • Ids are 16 lowercase hex characters
  • Records expire after 30 days and are cleaned up opportunistically on create/view
  • Unencrypted records store a markdown string
  • Encrypted records store only AES-256-GCM ciphertext plus PBKDF2-SHA256 salt/nonce/tag metadata; the password is never stored
  • hard_breaks controls whether single paragraph newlines render as <br> instead of spaces

data/etc/

wip

  • Plain text maintenance flag
  • Truthy values such as true, 1, yes, on, enabled, or wip enable maintenance mode
  • Enforced server-side by lib/session.php and lib/render.php; non-admin requests redirect to /error/wip

webhooks.json

Used key:

{
  "discord_feed": "https://discord.com/api/webhooks/..."
}

Toast Data

Toast configuration, personality, AI behavior, notification state, approvals, DM history, and internal service endpoints are documented on Toast.

notification-inbox-state.json

  • Stores in-site notification read and dismissal state
  • identities keys are prefixed with account: for lowercase usernames or guest: for browser-local guest inbox identities
  • Each identity retains up to 4,000 readKeys and dismissedKeys plus an updatedAt timestamp; feed notification content is derived from feed and reply records rather than duplicated in this file

notification-revision.txt

  • Opaque revision value updated after notification-producing feed/targeted writes and inbox read/dismissal changes
  • The lightweight revision endpoint reads this single small file so browsers do not repeatedly rebuild or poll their full inboxes

off-topic-archive.json

  • Discord export blob used by the archive viewer

page_views.json

Shape is roughly:

{
  "pages": {
    "/": {
      "count": 12,
      "visitors": {
        "<sha256>": 1730931224
      }
    }
  },
  "updated_at": "2026-03-02T00:00:00Z"
}

data/downloads/

  • Downloadable binaries, archives, presets, and similar files linked from the site

Clone this wiki locally