Select the types of activity you want to include in your feed.
docs: add multi-source support design
Design for adding source metadata (type, network, channel, user ID, display name) to links, quotes, and images. Enables per-source duplicate detection and API filtering for multi-community support.
···11+# Multi-Source Support
22+33+**Date:** 2026-02-13
44+**Status:** Proposed
55+66+## Problem
77+88+Tumble currently serves a single community with no tracking of where
99+posts originate. As usage expands to multiple IRC channels, Slack
1010+teams, and potentially Discord servers, posts need metadata about
1111+their source. This enables per-source duplicate detection, per-source
1212+filtering, and eventually source-scoped UI views.
1313+1414+## Solution
1515+1616+Add source metadata columns to all three content tables (links,
1717+quotes, images). Duplicate detection becomes scoped per source.
1818+API endpoints accept optional source fields on creation and support
1919+filtering by source on reads. No UI changes in this phase.
2020+2121+## Data Model
2222+2323+Five nullable columns added to `ircLink`, `quote`, and `image`:
2424+2525+| Column | Type | Purpose |
2626+|--------|------|---------|
2727+| `source_type` | VARCHAR(50) | Platform: "irc", "slack", "discord", "api", "web" |
2828+| `source_network` | VARCHAR(255) | IRC server, Slack team ID, Discord guild ID |
2929+| `source_channel` | VARCHAR(255) | IRC channel, Slack/Discord channel ID |
3030+| `source_user_id` | VARCHAR(255) | Platform-specific user ID (null for IRC) |
3131+| `source_user_name` | VARCHAR(255) | Display/mention name at time of post (null for IRC) |
3232+3333+Composite index on `(source_type, source_network, source_channel)`
3434+on each table.
3535+3636+The existing `user` column is unchanged. It continues to store the
3737+poster's name. The new `source_user_id` and `source_user_name` fields
3838+provide platform-specific identity alongside it.
3939+4040+Platform mapping:
4141+4242+| Field | IRC | Slack | Discord |
4343+|-------|-----|-------|---------|
4444+| `source_type` | "irc" | "slack" | "discord" |
4545+| `source_network` | server hostname | team ID | guild ID |
4646+| `source_channel` | channel name | channel ID | channel ID |
4747+| `source_user_id` | null | Slack user ID | Discord user ID |
4848+| `source_user_name` | null | mention name | display name |
4949+5050+GORM model additions (same for all three structs):
5151+5252+```go
5353+SourceType *string `json:"source_type,omitempty" gorm:"column:source_type"`
5454+SourceNetwork *string `json:"source_network,omitempty" gorm:"column:source_network"`
5555+SourceChannel *string `json:"source_channel,omitempty" gorm:"column:source_channel"`
5656+SourceUserID *string `json:"source_user_id,omitempty" gorm:"column:source_user_id"`
5757+SourceUserName *string `json:"source_user_name,omitempty" gorm:"column:source_user_name"`
5858+```
5959+6060+Pointer types for correct null handling and `omitempty` serialization.
6161+6262+## DDL
6363+6464+GORM AutoMigrate handles schema changes from the struct definitions.
6565+No manual DDL required. Columns are added automatically on next
6666+application startup.
6767+6868+## Data Backfill
6969+7070+All existing rows predate multi-source support and originate from IRC.
7171+A standalone SQL script (run once, manually) backfills them:
7272+7373+```sql
7474+UPDATE ircLink SET source_type = 'irc', source_network = 'jameswhite.org', source_channel = '#soggies' WHERE source_type IS NULL;
7575+UPDATE quote SET source_type = 'irc', source_network = 'jameswhite.org', source_channel = '#soggies' WHERE source_type IS NULL;
7676+UPDATE image SET source_type = 'irc', source_network = 'jameswhite.org', source_channel = '#soggies' WHERE source_type IS NULL;
7777+```
7878+7979+No backfill code in the application. No dead code paths.
8080+8181+## Duplicate Detection
8282+8383+Currently global: any POST of an existing URL returns 208. Changes to
8484+be scoped by `source_type + source_network + source_channel`.
8585+8686+**Rules:**
8787+- POST with source fields: check duplicates within that source tuple
8888+ only. Same URL from a different source is a new link (201).
8989+- POST with null/omitted source fields: fall back to global duplicate
9090+ check (backward compatibility).
9191+- Same URL within same source: 208 Already Reported with previous
9292+ submission info, as before.
9393+9494+**Example:**
9595+1. `https://example.com` from `slack / T12345 / C67890` -> 201
9696+2. Same URL from `irc / jameswhite.org / #soggies` -> 201
9797+3. Same URL from `slack / T12345 / C67890` again -> 208
9898+9999+Images: same scoping applied to MD5 deduplication.
100100+101101+Quotes: no duplicate detection currently, no change.
102102+103103+## API Changes
104104+105105+### POST Endpoints
106106+107107+`POST /api/v1/links`, `POST /api/v1/quotes` accept new optional
108108+fields:
109109+110110+```json
111111+{
112112+ "url": "https://example.com",
113113+ "user": "stahnma",
114114+ "source_type": "slack",
115115+ "source_network": "T12345",
116116+ "source_channel": "C67890",
117117+ "source_user_id": "U99999",
118118+ "source_user_name": "stahnma"
119119+}
120120+```
121121+122122+All source fields are optional. Omitting them stores nulls.
123123+124124+### GET Endpoints
125125+126126+`GET /api/v1/links`, `GET /api/v1/quotes`, `GET /api/v1/search` gain
127127+three optional query parameters:
128128+129129+- `source_type` - filter by platform
130130+- `source_network` - filter by team/server (requires source_type)
131131+- `source_channel` - filter by channel (requires source_type +
132132+ source_network)
133133+134134+Cumulative filtering. Omitting all returns everything (current
135135+behavior).
136136+137137+### Response Payloads
138138+139139+Source fields included with `omitempty`. Null source fields are
140140+omitted from responses:
141141+142142+```json
143143+{
144144+ "id": 1234,
145145+ "url": "https://example.com",
146146+ "user": "stahnma",
147147+ "title": "Example",
148148+ "source_type": "slack",
149149+ "source_network": "T12345",
150150+ "source_channel": "C67890",
151151+ "source_user_id": "U99999",
152152+ "source_user_name": "stahnma"
153153+}
154154+```
155155+156156+## Store Interface Changes
157157+158158+`GetLinkByURL` becomes source-scoped. New filter struct for query
159159+methods:
160160+161161+```go
162162+type SourceFilter struct {
163163+ SourceType *string
164164+ SourceNetwork *string
165165+ SourceChannel *string
166166+}
167167+```
168168+169169+Used by `GetLinks`, `GetQuotes`, `GetImages`, and the duplicate
170170+detection lookup. When all fields are nil, behaves identically to
171171+current implementation.
172172+173173+`TimelineItem` gains source fields for future frontend use.
174174+175175+## Testing
176176+177177+- Duplicate detection: same URL + same source = 208; same URL +
178178+ different source = 201; null source = global fallback
179179+- Filtering: source params return correct subset; params combine to
180180+ narrow results; no params returns everything
181181+- Backfill: existing rows have correct values after running script
182182+- Serialization: source fields omitted from JSON when null; present
183183+ when populated
184184+- Round-trip: POST with source fields, GET returns them; POST
185185+ without, GET omits them
186186+187187+## API Documentation
188188+189189+Update `internal/assets/openapi.json` to reflect all API changes:
190190+191191+- Add `source_type`, `source_network`, `source_channel`,
192192+ `source_user_id`, and `source_user_name` to request schemas for
193193+ `POST /api/v1/links` and `POST /api/v1/quotes`
194194+- Add `source_type`, `source_network`, `source_channel` as optional
195195+ query parameters on `GET /api/v1/links`, `GET /api/v1/quotes`, and
196196+ `GET /api/v1/search`
197197+- Add source fields to all response schemas (links, quotes, images)
198198+- Document the scoped duplicate detection behavior (208 is now
199199+ per-source when source fields are provided)
200200+201201+## Out of Scope
202202+203203+- UI changes
204204+- Authentication/authorization per source
205205+- User identity lookup table for name history tracking
206206+- Slack, Discord, or other client/bot implementations
207207+- Changes to the existing `source` request parameter (controls
208208+ response format for irc/api/html — separate concept)