-
Notifications
You must be signed in to change notification settings - Fork 0
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.
Runtime data lives under /data.
Repo intent:
-
/datais not supposed to be versioned normally -
.gitignoreignores/data/**/* -
.rsyncignoreexcludes/data/**from deployment sync
So local/dev/prod data has to be managed separately.
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}; legacynewsletter:{id}values can exist but are ignored -
theme: defaultis Blackprint and uses the base template plus/style.css;theme: classicenables savedcolorsforbg/fg/border/subtle/links;theme: ambercrtis shown asCRTand uses only savedcolors.linksas its main phosphor color; any other valid value refers to a/themes/{theme-id}.jsonfile withname,description,thumbnail,html, andcss - Legacy
blackprintnormalizes todefault,customnormalizes toclassic,newsprintnormalizes towhiteprint,crtnormalizes toambercrt, and removedliminal/syswavepreferences normalize todefault - Text glow is stored in
glowIntensity; the settings UI writesnonefor off andmediumfor on, while legacylow/highvalues are treated as enabled medium glow when saved again - Title motion is stored in
titleAnimation(wobble,bounce,rubberhose,bubble,slot-machine,moonwalk, orheartbeat),titleAnimationAlways(boolean), andtitleAnimationDesync(boolean, defaulttrue); removedpinballvalues migrate towobble; legacyorbit,domino, andlava-lampvalues migrate tobubble; removedtidal-wave,accordion, andtypewritervalues migrate toslot-machine, whilehelicopter,haunted, andjugglemigrate tomoonwalk; 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
browserNotificationsEnabledandjournalBrowserNotificationsEnabledkeys 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 -
mustResetPasswordis used by the shared session bootstrap to force first-login password changes -
postingRestrictedis 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 -
discordUserIdlinks a site account to a Discord member for bot DMs and notifications -
emailAddressmarks accounts with a fridge.dev email mailbox; when present and valid, shared chrome swaps the footer Discord button to/account/email, and/account/emailshows the assigned address -
allowedPagescurrently includes functional grants likefeed,journal,comments, andchat
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
attachmentobject with encrypted blob metadata:id,name,mime, andsize; 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
replyTowith another message id, plusreactionskeyed by valid emoji sequences with active viewer roles such asmanagerorparticipant - Conversations may include
participantUsernamewhen a logged-in account claims the invite, orparticipantHashwhen an anonymous browser cookie claims it; the first-open popup copy changes based on that claim type - Conversations may include
recipientIntroSeenAtonce 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
allowedPagescontainingchatcan create, view, and delete conversations without claiming the recipient slot - Deleting a conversation unlinks the encrypted JSON file immediately
- Encryption uses
FRIDG3_CHAT_KEYwhen set; otherwise the app createsdata/chat/.chat_key - Lightweight presence indicators use sidecar files under
data/chat/.presence/{conversationId}.json; current entries storelastSeen,active, and a short-livedtypingUntil, 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
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.jsonbecomescool -
nameis the label shown in/settings -
descriptionis the short supporting text shown under the theme name in the picker -
thumbnailis a 4:3 preview path relative to/themes, usuallythumbnails/{theme-id}.svg -
htmlandcssmust be relative paths in/themes/lib, for exampleaero/aero.htmlandaero/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.htmland only swaps the CSS
- Map of client IP -> unix timestamp array
- Used for login throttling
Legacy feed post format:
@usernameYYYY-MM-DD HH:MM:SS- Body text / BBCode
Version 2 feed post format:
v2@usernameYYYY-MM-DD HH:MM:SS- 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.tomlis 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.
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: trueplus a plaintextip; guest display names are stored inusername, default toAnonymous, cannot match a registered account username case-insensitively, and are filtered with guest reply bodies through/feed/filters/*.txtbefore storage; matching body text becomes tooltip-wrapped★text explainingthis 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
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.
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.
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.
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.
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.
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.
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.
Legacy .txt journal post:
YYYY-MM-DD- Title
- Description
- Optional
CARD_IMAGE:<url> - Trusted HTML body
New journal posts are {id}.md files:
- Exact
v2version marker - YAML front matter containing
title,description,date, and optionalcard_image; journal posts do not setauthor - 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:
USER:<username>- Title
- Description
- Optional
FORMAT:htmlorFORMAT:markdown - 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.
Entry format:
- Timestamp
- Display name
- Optional
IP:<address>metadata for new entries - Message body (line 3 for legacy or sanitized entries without IP metadata)
Plus:
-
ip_index.jsonfor 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/guestbookand its descendants; entries are exposed only through the PHP guestbook and admin moderation views
- 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.
Artist folders currently include:
frdg3cactile
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.
- Track files referenced by music metadata
- Also used by shared playback features
-
data/audio/voice/stores public feed voice notes as compressed.m4afiles -
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 legacydata/audio/attachments/path remains recognized for cleanup of older posts
- Stores feed and journal video attachments for the inline, site-styled video player; its controls overlay the video and attachment filenames are not displayed
- Private contact submissions as
{YYYYMMDDHHMMSS}_{random}.json - Each submission stores
id,createdAt, hashed IP, user agent, name, email, message, notification channel id, and optionalnotifyError - Successful submissions also append one targeted in-site notification per admin account to
data/etc/targeted-notifications.json -
rate_limits.jsonstores 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=1route
- 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
markdownstring - Encrypted records store only AES-256-GCM ciphertext plus PBKDF2-SHA256 salt/nonce/tag metadata; the password is never stored
-
hard_breakscontrols whether single paragraph newlines render as<br>instead of spaces
- Plain text maintenance flag
- Truthy values such as
true,1,yes,on,enabled, orwipenable maintenance mode - Enforced server-side by
lib/session.phpandlib/render.php; non-admin requests redirect to/error/wip
Used key:
{
"discord_feed": "https://discord.com/api/webhooks/..."
}Toast configuration, personality, AI behavior, notification state, approvals, DM history, and internal service endpoints are documented on Toast.
- Stores in-site notification read and dismissal state
-
identitieskeys are prefixed withaccount:for lowercase usernames orguest:for browser-local guest inbox identities - Each identity retains up to 4,000
readKeysanddismissedKeysplus anupdatedAttimestamp; feed notification content is derived from feed and reply records rather than duplicated in this file
- 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
- Discord export blob used by the archive viewer
Shape is roughly:
{
"pages": {
"/": {
"count": 12,
"visitors": {
"<sha256>": 1730931224
}
}
},
"updated_at": "2026-03-02T00:00:00Z"
}- Downloadable binaries, archives, presets, and similar files linked from the site