-
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 Account Restrictions and IP Bans.
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,
"browserNotificationsEnabled": true,
"journalBrowserNotificationsEnabled": 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 -
browserNotificationsEnabledstores the account-backed preference for browser feed notifications;journalBrowserNotificationsEnabledstores the account-backed preference for new journal post browser notifications; logged-in notification dedupe state is stored indata/etc/feed-browser-notify-state.json, while logged-out browsers keep the same preferences and dedupe state in localStorage -
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, BBCode 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
feed post format:
@usernameYYYY-MM-DD HH:MM:SS- body text / BBCode
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, transcoded to small mono .m4a files, and stored under data/audio/voice/.
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 BBCode",
"parentId": "optional parent reply id for comment replies",
"isGuest": true,
"ip": "203.0.113.10",
"guestBrowserId": "optional same-browser notification token"
}
]
}notes:
- reply ids are generated on write; older data may be normalized into
legacy_*ids at read time - 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 token used only so guests can receive browser notifications when someone replies to their comments from another browser/account - reply bodies can contain image BBCode that points at
/data/images/* - new reply bodies can also contain voice note audio BBCode that points at
/data/audio/voice/* - 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 - sanitized developer data copies blank guest reply
ipvalues, remove guest reply browser notification tokens, removeIP:metadata from guestbook entry files, clear guestbook ownership mappings, and clear the shared feed/guestbook IP ban list before the copy is zipped - automatic Toast replies are stored as normal
username: "toast"replies when a user replies to Toast's post or mentions@toast; generated Toast replies begin by mentioning the triggering user and are delayed by 1 minute before posting
IP-keyed posting ban list shared by feed replies, guestbook submissions, the contact form, mdpaste creation, and upload room/signaling use, and managed through /settings/guests.
- entries can record ban metadata and usernames seen for that IP
- purging an IP's feed replies and guestbook posts is a separate action and does not add, remove, or mutate ban entries
- the manage guests page also scans
data/feed/replies/*.jsonforisGuest: truereplies and groups them by plaintextip
Site-wide hard-ban list managed by the admin-only /settings/banned-ips editor.
- contains exact IPv4 or IPv6 addresses separated by whitespace; saves normalize the list to one address per line and reject invalid tokens
- this list is separate from posting bans in
data/feed/banned_ips.json - nginx checks it through the internal hard-ban authorization endpoint before serving pages or static files and redirects matches to
/error/blacklisted - direct web access to the list and to the checker is blocked;
/error/blacklisteduses self-contained local templates because hard-banned clients cannot load global website assets - hard-banned clients may additionally load font files beneath
/resources; all other shared assets remain unavailable - the developer-data sanitizer empties this file and the publishing archive excludes it entirely
Private browser-to-IP associations used to carry an active hard ban across IP changes.
- a random first-party identifier is mirrored in a five-year cookie and browser local storage; the server stores its original manually banned IP, observed IPs, timestamps, and a hash of the user agent
- when an identifier tied to an active original IP appears from a new IP, the new IP is appended to
hard-banned-ips.txt - removing the original IP through
/settings/banned-ipsremoves every automatically associated IP and the corresponding identifier records - direct client access is blocked; the developer-data sanitizer replaces the file with an empty
identitiesobject and the publishing archive excludes it entirely
Read-only hard-ban source lists. Every valid whitespace-separated IPv4 or IPv6 address from every .txt file directly inside this directory is merged with the manual hard-ban list at request time.
- source IPs are enforced without appearing in the
/settings/banned-ipstextarea or being copied tohard-banned-ips.txt - unreadable files and invalid tokens are ignored without disabling the other source lists
- nginx blocks the entire directory from client access
- the developer-data sanitizer clears the directory and the publishing archive excludes its contents
published journal post:
YYYY-MM-DD- title
- description
- trusted HTML body
draft format:
USER:<username>- title
- description
- optional
FORMAT:html - draft body
without FORMAT:html, preview treats the body as BBCode.
with it, preview treats the body as raw HTML.
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 - nginx blocks direct client access to
/data/guestbookand its descendants; entries are exposed only through the PHP guestbook and admin moderation views - the sanitizer removes entry
IP:metadata and clearsip_index.jsonwhile retaining the public guestbook messages
- uploaded images used across feed, journal, and gallery content
- expected web path is
/data/images/<filename>
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
- private contact submissions as
{YYYYMMDDHHMMSS}_{random}.json - each submission stores
id,createdAt, hashed IP, user agent, name, email, message, notification channel id, and optionalnotifyError -
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/..."
}expected shape:
{
"bot": { "token": "...", "client_id": "...", "status": "online|offline" },
"stream": { "url": "http(s)://...", "name": "..." },
"channel": { "id": "...", "name": "..." },
"features": { "auto_play": true, "loop": true },
"groq": {
"api_key": "...",
"model": "llama-3.1-8b-instant",
"website_model": "llama-3.3-70b-versatile",
"vision_model": "meta-llama/llama-4-scout-17b-16e-instruct",
"temperature": 0.8,
"top_p": 0.95,
"max_completion_tokens": 700,
"timeout_seconds": 30,
"max_history_messages": 12,
"max_vision_images": 5
}
}groq powers Toast's AI replies to direct messages, the hardcoded Toast feed generator, and automatic Toast feed replies. If api_key is empty, Toast still logs inbound DMs but skips the AI reply, the feed generator returns an error instead of drafting, and automatic feed replies are skipped. model defaults to llama-3.1-8b-instant and is used by the Discord Toast DM flow. website_model defaults to llama-3.3-70b-versatile and is used by website Toast feed generation and automatic feed replies; legacy feed_model is also accepted as a fallback name. Image/GIF DMs use vision_model, defaulting to Groq's meta-llama/llama-4-scout-17b-16e-instruct. max_history_messages controls how many recent logged DM messages are sent as conversation context, and max_vision_images caps image attachments at Groq's 5-image request limit. Toast also sends Groq a compact summary of its bot duties, including radio playback, slash-command radio controls, account-linking support, and automated notification DMs. When a DM appears to ask about fridge.dev, Toast can also send small relevant context from wiki/Home.md and wiki/Routes-and-Features.md to Groq so replies can describe the site without sounding like developer docs.
Website feed generation adds stricter feed-only voice limits on top of the editable personality: Toast feed posts and automatic replies should stay short, closer to old-style Twitter posts than assistant answers. Automatic replies are capped after cleanup so an ignored prompt cannot turn into a forum-moderator essay.
Website and Discord AI prompts also append an identity anchor after loading editable personality JSON: Toast should describe themself as a humanoid fridge.dev resident whose closest relative is the homosapien, not as a bot, AI, assistant, agent, model, or program.
The website Toast feed generator and auto replies add feed-only wording rails to reduce repetitive openings like just did, just made, just got, just found, or just realized. Website feed post generation accepts a 1-5 length setting with deliberately strong steps: one-liner, short, normal, ramble, and trauma dump. The selected length applies both prompt instructions and cleanup caps, with max allowing a much longer vulnerable post while the shortest setting is forced down to a single short sentence. Feed generation sends only a small weak style sample from already-published non-Toast feed posts, never unpublished generated drafts, and instructs Groq not to reuse those samples' topics or phrasing. Recent published Toast posts are sent only as negative examples to avoid repeating their topics, imagery, openings, or emotional arc. Each generation also gets a private freshness seed with a random creative angle, texture, and anti-pattern so repeated clicks vary more. Toast feed posts are instructed to be self-contained personal thoughts rather than conversation starters, so they should not ask readers for feedback, replies, comments, validation, or suggestions, and should not acknowledge audience size or being alone. The website no longer stores its own Groq cooldown state; Groq 429s are returned directly after any short one-shot retry.
AI DM replies are split into sentence-aware Discord messages, usually 2-4 sentences per send depending on sentence length, while still staying below Discord's hard message limit. Toast waits at least 5 seconds before every AI reply chunk so the visible typing state never flashes and instantly dumps a response. Each Discord user has one active AI reply task: if another DM arrives while Toast is generating or pacing an unsent chunk, Toast cancels the unfinished reply and regenerates from the queued inbound DMs combined into one chronological prompt.
Toast's AI prompt includes an exact Discord slash-command allow-list: /play, /stop, /status, /sendmsg, and /shareupdate. Website paths such as /feed must be described as fridge.dev pages, not Discord slash commands.
shared editable Toast personality source:
{
"discord": {
"system_prompt": "core Discord Toast personality instructions",
"style_rules": ["optional behavior/style rule"],
"do_not": ["optional constraint"],
"private_lore": "optional guarded lore"
},
"feed": {
"system_prompt": "core feed-writing Toast personality instructions",
"style_rules": ["optional behavior/style rule"],
"do_not": ["optional constraint"],
"private_lore": "optional guarded lore"
}
}/settings exposes this JSON only to the hardcoded toast session. Both discord and feed must be objects with non-empty system_prompt values. If the file is missing, website code seeds both blocks from others/toast-discord-bot/bot/personality.json in memory.
expected shape:
{
"system_prompt": "core Toast personality instructions",
"style_rules": ["optional behavior/style rule"],
"do_not": ["optional constraint"],
"private_lore": "optional lore that Toast only shares when directly asked"
}legacy Discord bot fallback. The bot now prefers data/etc/toast-personality.json and uses this file only when the shared file is missing or lacks a usable discord block. If both are missing, empty, or invalid, the bot logs a warning and uses a small built-in Toast fallback prompt. private_lore is included in the system prompt with an explicit guardrail to avoid volunteering it unless the user asks about Toast's origin, lore, backstory, life, or purpose.
- array of timestamped bot status entries
- internal bot dedupe state for sent feed mention/reply notifications
- stores which feed post mentions, feed reply mentions, and replies to a user's own posts have already triggered DMs
- internal browser-notification dedupe state for logged-in users
- stores per-account
seenKeysfor feed mention/reply and journal notification events so a fresh browser login does not replay all historical matching events - sanitized developer data replaces it with an empty
usersobject
- tracked inbound/outbound DM threads used by
/others/toast-discord-bot/messages - stores per-user profile snapshot data, optional
ai_mutedreply-suppression state, plus message history - an inbound DM containing exactly
CLEARMEMORYacts as a memory boundary for AI replies; future Groq context only includes messages after the newest boundary
- the toast bot exposes localhost-only
POST /contact/notifyon127.0.0.1:8765 -
/contactcalls it after saving a submission - toast sends the alert to Discord channel
1503931489560301609
- the toast bot exposes localhost-only
POST /patch-noticeon127.0.0.1:8765 - the deploy workflow calls it after a successful deploy to
main - toast sends the fully formatted patch notice preview to approval channel
1526075637096255548and reacts to its own message with✅ - when an admin approves with
✅, toast posts the Discord embed patch note to channel1455194403642802309 - approved patch notices ping role
1408064850688475197in the update channel so the update gets the right attention - the payload includes the shipped commit range plus a PR link when the update came from a merged pull request
- 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