Miniflux

The schema behind Miniflux, a minimalist feed reader, rebuilt from the migration strings in its Go source: users and how they sign in, the feeds and categories they follow, the entries fetched for them, and a few system tables.

16 tables · 272 columns · 14 foreign keys · 4 domains  |  1 errors · 6 warnings · 9 notes

Generated from v2@2.3.3.sql on 2026-09-05.

Conventions

Conventions are claims. Findings below are where the schema breaks them.

Findings

Errors (1)

F001schema_versionprimary-key

No primary key

Without a primary key rows are not individually addressable: no safe UPDATE/DELETE of one row, no logical replication, ORMs misbehave, and duplicates are legal.

Fix Add a natural composite key or a surrogate id.

ALTER TABLE schema_version ADD PRIMARY KEY (...);

Warnings (6)

F003enclosures entry_idfk-index

Foreign key without index

PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on entries must scan enclosures to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own. An index is not free. Every write to enclosures maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often entries rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.

Fix Add an index on the FK column(s).

CREATE INDEX CONCURRENTLY idx_enclosures_entry_id ON enclosures(entry_id);
F007enclosures mime_typeundocumented-enum

Enum-like column with no CHECK

mime_type is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.

Fix Add a CHECK constraint (cheap, easy to evolve) or a lookup table if values need metadata. Avoid native ENUM types unless the set is truly frozen.

ALTER TABLE enclosures ADD CONSTRAINT enclosures_mime_type_check CHECK (mime_type IN (...));
F004feed_icons icon_idfk-index

Foreign key without index

PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on icons must scan feed_icons to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own. An index is not free. Every write to feed_icons maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often icons rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.

Fix Add an index on the FK column(s).

CREATE INDEX CONCURRENTLY idx_feed_icons_icon_id ON feed_icons(icon_id);
F002feeds category_idfk-index

Foreign key without index

PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on categories must scan feeds to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own. An index is not free. Every write to feeds maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often categories rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.

Fix Add an index on the FK column(s).

CREATE INDEX CONCURRENTLY idx_feeds_category_id ON feeds(category_id);
F008icons mime_typeundocumented-enum

Enum-like column with no CHECK

mime_type is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.

Fix Add a CHECK constraint (cheap, easy to evolve) or a lookup table if values need metadata. Avoid native ENUM types unless the set is truly frozen.

ALTER TABLE icons ADD CONSTRAINT icons_mime_type_check CHECK (mime_type IN (...));
F005webauthn_credentials user_idfk-index

Foreign key without index

PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan webauthn_credentials to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own. An index is not free. Every write to webauthn_credentials maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.

Fix Add an index on the FK column(s).

CREATE INDEX CONCURRENTLY idx_webauthn_credentials_user_id ON webauthn_credentials(user_id);

Notes (9)

F012acme_cacheorphan-table

Isolated table

acme_cache references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.

Fix Document its purpose or drop it.

F015feedswide-table

Wide table (42 columns)

Tables this wide usually hide several entities (or a JSON column that wants to be one). Every row update rewrites the whole tuple; TOAST kicks in; indexes bloat.

Fix Look for column groups that always change together and split them out.

F016integrationswide-table

Wide table (122 columns)

Tables this wide usually hide several entities (or a JSON column that wants to be one). Every row update rewrites the whole tuple; TOAST kicks in; indexes bloat.

Fix Look for column groups that always change together and split them out.

F009schema_versionorphan-table

Isolated table

schema_version references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.

Fix Document its purpose or drop it.

F010sessionsorphan-table

Isolated table

sessions references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.

Fix Document its purpose or drop it.

F011tokensorphan-table

Isolated table

tokens references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.

Fix Document its purpose or drop it.

F013usersblast-radius

Hub table: referenced by 8 tables

Any change to its key, its delete semantics, or its partitioning touches every dependent. Migrations on hub tables need the longest lock windows and the most careful rollout.

Fix Treat schema changes here as breaking changes with a written rollout plan.

F014userswide-table

Wide table (32 columns)

Tables this wide usually hide several entities (or a JSON column that wants to be one). Every row update rewrites the whole tuple; TOAST kicks in; indexes bloat.

Fix Look for column groups that always change together and split them out.

F006web_sessions user_idfk-nullable

Nullable foreign key

web_sessions.user_id may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.

Fix Confirm the optionality is a domain rule; document it in the column comment.

Schema

Schema map: 16 tables, 14 foreign keys, 4 domains. Click to open the 3D explorer.Users and accesssessionsusersapi_keysintegrationsweb_sessionswebauthn_credentialsFeedsiconscategoriesfeedsfeed_iconsEntriesentriesentry_tombstonesenclosuresSystemacme_cacheschema_versiontokens

16 tables · 14 foreign keys · 4 domains. Open the 3D explorer: rotate, zoom, click any table or relationship.

Users and access 6 tables

Users, their browser and API sessions, API keys, passkeys, and per-user third-party integrations.

Entity-relationship diagram: users, sessions, web_sessions, api_keys, webauthn_credentials, integrationsusersid PKserialusernametextpasswordtextis_adminboollanguagetexttimezonetextthemetextlast_login_attimestamptzentry_directionentry_sorting_directionkeyboard_shortcutsbooleanentries_per_pageintegershow_reading_timebooleanentry_swipebooleanstylesheettextgoogle_idtextopenid_connect_idtextdisplay_modewebapp_display_modeentry_orderentry_sorting_orderdefault_reading_speedintegercjk_reading_speedintegerdefault_home_pagetextcategories_sorting_ordertextdouble_tapbooleanmark_read_on_viewbooleanmedia_playback_ratenumericblock_filter_entry_rulestextkeep_filter_entry_rulestextmark_read_on_media_player_completionboolcustom_jstextexternal_font_hoststextalways_open_external_linksboolopen_external_links_in_new_tabboolsessionsid PKtextdatajsonbcreated_attimestamptzweb_sessionsid PKtextsecret_hashbyteauser_id FKintegercreated_attimestamptzuser_agenttextipinetstatejsonbapi_keysid PKserialuser_id FKintegertokentextdescriptiontextlast_used_attimestamptzcreated_attimestamptzwebauthn_credentialshandle PKbyteacred_idbyteauser_id FKintegerpublic_keybyteaattestation_typevarchar(255)aaguidbyteasign_countbigintclone_warningboolnametextadded_ontimestamptzlast_seen_ontimestamptzbackup_eligiblebooleanbackup_statebooleanintegrationsuser_id PKintegerpinboard_enabledboolpinboard_tokentextpinboard_tagstextpinboard_mark_as_unreadboolinstapaper_enabledboolinstapaper_usernametextinstapaper_passwordtextfever_enabledboolfever_usernametextfever_tokentextwallabag_enabledboolwallabag_urltextwallabag_client_idtextwallabag_client_secrettextwallabag_usernametextwallabag_passwordtextnunux_keeper_enabledboolnunux_keeper_urltextnunux_keeper_api_keytexttelegram_bot_enabledbooltelegram_bot_tokentexttelegram_bot_chat_idtextgooglereader_enabledboolgooglereader_usernametextgooglereader_passwordtextespial_enabledboolespial_urltextespial_api_keytextespial_tagstextlinkding_enabledboollinkding_urltextlinkding_api_keytextwallabag_only_urlboolmatrix_bot_enabledboolmatrix_bot_usertextmatrix_bot_passwordtextmatrix_bot_urltextmatrix_bot_chat_idtextlinkding_tagstextlinkding_mark_as_unreadboolnotion_enabledboolnotion_tokentextnotion_page_idtextreadwise_enabledboolreadwise_api_keytextapprise_enabledboolapprise_urltextapprise_services_urltextshiori_enabledboolshiori_urltextshiori_usernametextshiori_passwordtextshaarli_enabledboolshaarli_urltextshaarli_api_secrettextwebhook_enabledboolwebhook_urltextwebhook_secrettexttelegram_bot_topic_idintegertelegram_bot_disable_web_page_previewbooltelegram_bot_disable_notificationbooltelegram_bot_disable_buttonsboolrssbridge_enabledboolrssbridge_urltextomnivore_enabledboolomnivore_api_keytextomnivore_urltextlinkace_enabledboollinkace_urltextlinkace_api_keytextlinkace_tagstextlinkace_is_privateboollinkace_check_disabledboollinkwarden_enabledboollinkwarden_urltextlinkwarden_api_keytextreadeck_enabledboolreadeck_only_urlboolreadeck_urltextreadeck_api_keytextreadeck_labelstextraindrop_enabledboolraindrop_tokentextraindrop_collection_idtextraindrop_tagstextbetula_urltextbetula_tokentextbetula_enabledboolntfy_enabledboolntfy_urltextntfy_topictextntfy_api_tokentextntfy_usernametextntfy_passwordtextntfy_icon_urltextcubox_enabledboolcubox_api_linktextdiscord_enabledbooldiscord_webhook_linktextntfy_internal_linksboolslack_enabledboolslack_webhook_linktextpushover_enabledboolpushover_usertextpushover_tokentextpushover_devicetextpushover_prefixtextrssbridge_tokentextkarakeep_enabledboolkarakeep_api_keytextkarakeep_urltextlinktaco_enabledboollinktaco_api_tokentextlinktaco_org_slugtextlinktaco_tagstextlinktaco_visibilitylinktaco_link_visibilitywallabag_tagstextarchiveorg_enabledboolkarakeep_tagstextlinkwarden_collection_idintegerreadeck_push_enabledbooluser_iduser_iduser_iduser_id

Relationships

users

32 cols · no RLS · line 5View in 3D
  • Hub table: referenced by 8 tables — Any change to its key, its delete semantics, or its partitioning touches every dependent. Migrations on hub tables need the longest lock windows and the most careful rollout.
  • Wide table (32 columns) — Tables this wide usually hide several entities (or a JSON column that wants to be one). Every row update rewrites the whole tuple; TOAST kicks in; indexes bloat.
ColumnTypeNullDefaultReferencesNotes
id PKserialNOT NULL
username UKtextNOT NULL
password textnull
is_admin boolnull'f'
language textnull'en_US'
timezone textnull'UTC'
theme textnull'default'
last_login_at timestamptznull
entry_direction entry_sorting_directionnull
keyboard_shortcuts booleannull
entries_per_page integernull
show_reading_time booleannull
entry_swipe booleannull
stylesheet textnull
google_id textnull
openid_connect_id textnull
display_mode webapp_display_modenull
entry_order entry_sorting_ordernull
default_reading_speed integernull
cjk_reading_speed integernull
default_home_page textnull
categories_sorting_order textnull
double_tap booleannull
mark_read_on_view booleannull
media_playback_rate numericnull
block_filter_entry_rules textnull
keep_filter_entry_rules textnull
mark_read_on_media_player_completion boolnull
custom_js textnull
external_font_hosts textnull
always_open_external_links boolnull
open_external_links_in_new_tab boolnull

Indexes

  • UNIQUE (google_id) WHERE google_id <> '' users_google_id_idx
  • UNIQUE (openid_connect_id) WHERE openid_connect_id <> '' users_openid_connect_id_idx

Referenced by

sessions

3 cols · no RLS · line 141View in 3D
  • Isolated table — sessions references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
ColumnTypeNullDefaultReferencesNotes
id PKtextNOT NULL
data jsonbNOT NULL
created_at timestamptzNOT NULLnow()

Indexes

  • none

Referenced by

  • nothing

web_sessions

7 cols · no RLS · line 595View in 3D
  • Nullable foreign key user_id — web_sessions.user_id may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
ColumnTypeNullDefaultReferencesNotes
id PKtextNOT NULL
secret_hash byteaNOT NULL
user_id integernullusers.id 1:N · CASCADE
created_at timestamptzNOT NULLnow()
user_agent textNOT NULL''
ip inetnull
state jsonbNOT NULLCAST('{}' AS jsonb)CHECK jsonb_typeof(state) = 'object'

Indexes

  • (user_id) WHERE user_id IS NOT NULL web_sessions_user_id_idx
  • (created_at) web_sessions_created_at_idx

Referenced by

  • nothing

Table checks: jsonb_typeof(state) = 'object'

api_keys

6 cols · no RLS · line 205View in 3D
ColumnTypeNullDefaultReferencesNotes
id PKserialNOT NULL
user_id integerNOT NULLusers.id 1:N · CASCADE
token UKtextNOT NULL
description textNOT NULL
last_used_at timestamptznull
created_at timestamptznullnow()

Indexes

  • none

Referenced by

  • nothing

webauthn_credentials

13 cols · no RLS · line 405View in 3D
  • Foreign key without index user_id — PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan webauthn_credentials to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own. An index is not free. Every write to webauthn_credentials maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
ColumnTypeNullDefaultReferencesNotes
handle PKbyteaNOT NULL
cred_id UKbyteaNOT NULL
user_id integerNOT NULLusers.id 1:N · CASCADE
public_key byteaNOT NULL
attestation_type varchar(255)NOT NULL
aaguid byteanull
sign_count bigintnull
clone_warning boolnull
name textnull
added_on timestamptznullnow()
last_seen_on timestamptznullnow()
backup_eligible booleannull
backup_state booleannull

Indexes

  • none

Referenced by

  • nothing

integrations

122 cols · no RLS · line 115View in 3D
  • Wide table (122 columns) — Tables this wide usually hide several entities (or a JSON column that wants to be one). Every row update rewrites the whole tuple; TOAST kicks in; indexes bloat.
ColumnTypeNullDefaultReferencesNotes
user_id PKintegerNOT NULLusers.id 1:1 · CASCADE
pinboard_enabled boolnull'f'
pinboard_token textnull''
pinboard_tags textnull'miniflux'
pinboard_mark_as_unread boolnull'f'
instapaper_enabled boolnull'f'
instapaper_username textnull''
instapaper_password textnull''
fever_enabled boolnull'f'
fever_username textnull''
fever_token textnull''
wallabag_enabled boolnull
wallabag_url textnull
wallabag_client_id textnull
wallabag_client_secret textnull
wallabag_username textnull
wallabag_password textnull
nunux_keeper_enabled boolnull
nunux_keeper_url textnull
nunux_keeper_api_key textnull
telegram_bot_enabled boolnull
telegram_bot_token textnull
telegram_bot_chat_id textnull
googlereader_enabled boolnull
googlereader_username textnull
googlereader_password textnull
espial_enabled boolnull
espial_url textnull
espial_api_key textnull
espial_tags textnull
linkding_enabled boolnull
linkding_url textnull
linkding_api_key textnull
wallabag_only_url boolnull
matrix_bot_enabled boolnull
matrix_bot_user textnull
matrix_bot_password textnull
matrix_bot_url textnull
matrix_bot_chat_id textnull
linkding_tags textnull
linkding_mark_as_unread boolnull
notion_enabled boolnull
notion_token textnull
notion_page_id textnull
readwise_enabled boolnull
readwise_api_key textnull
apprise_enabled boolnull
apprise_url textnull
apprise_services_url textnull
shiori_enabled boolnull
shiori_url textnull
shiori_username textnull
shiori_password textnull
shaarli_enabled boolnull
shaarli_url textnull
shaarli_api_secret textnull
webhook_enabled boolnull
webhook_url textnull
webhook_secret textnull
telegram_bot_topic_id integernull
telegram_bot_disable_web_page_preview boolnull
telegram_bot_disable_notification boolnull
telegram_bot_disable_buttons boolnull
rssbridge_enabled boolnull
rssbridge_url textnull
omnivore_enabled boolnull
omnivore_api_key textnull
omnivore_url textnull
linkace_enabled boolnull
linkace_url textnull
linkace_api_key textnull
linkace_tags textnull
linkace_is_private boolnull
linkace_check_disabled boolnull
linkwarden_enabled boolnull
linkwarden_url textnull
linkwarden_api_key textnull
readeck_enabled boolnull
readeck_only_url boolnull
readeck_url textnull
readeck_api_key textnull
readeck_labels textnull
raindrop_enabled boolnull
raindrop_token textnull
raindrop_collection_id textnull
raindrop_tags textnull
betula_url textnull
betula_token textnull
betula_enabled boolnull
ntfy_enabled boolnull
ntfy_url textnull
ntfy_topic textnull
ntfy_api_token textnull
ntfy_username textnull
ntfy_password textnull
ntfy_icon_url textnull
cubox_enabled boolnull
cubox_api_link textnull
discord_enabled boolnull
discord_webhook_link textnull
ntfy_internal_links boolnull
slack_enabled boolnull
slack_webhook_link textnull
pushover_enabled boolnull
pushover_user textnull
pushover_token textnull
pushover_device textnull
pushover_prefix textnull
rssbridge_token textnull
karakeep_enabled boolnull
karakeep_api_key textnull
karakeep_url textnull
linktaco_enabled boolnull
linktaco_api_token textnull
linktaco_org_slug textnull
linktaco_tags textnull
linktaco_visibility linktaco_link_visibilitynull
wallabag_tags textnull
archiveorg_enabled boolnull
karakeep_tags textnull
linkwarden_collection_id integernull
readeck_push_enabled boolnull

Indexes

  • none

Referenced by

  • nothing

Feeds 4 tables

Feeds and the categories that group them, plus the deduplicated icons feeds share.

Entity-relationship diagram: categories, feeds, icons, feed_iconscategoriesid PKserialuser_id FKintegertitletexthide_globallybooleanfeedsid PKbigserialuser_id FKintegercategory_id FKintegertitletextfeed_urltextsite_urltextchecked_attimestamptzetag_headertextlast_modified_headertextparsing_error_msgtextparsing_error_countintegerscraper_rulestextrewrite_rulestextcrawlerbooleanusernametextpasswordtextuser_agenttextdisabledbooleannext_check_attimestamptzignore_http_cacheboolfetch_via_proxyboolblocklist_rulestextkeeplist_rulestextallow_self_signed_certificatesbooleancookietexthide_globallybooleanurl_rewrite_rulestextno_media_playerbooleanapprise_service_urlstextdisable_http2booldescriptiontextntfy_enabledboolntfy_priorityintegerwebhook_urltextpushover_enabledboolpushover_priorityintegerntfy_topictextproxy_urltextblock_filter_entry_rulestextkeep_filter_entry_rulestextignore_entry_updatesboollanguagetexticonsid PKbigserialhashtextmime_typetextcontentbyteaexternal_idtextfeed_iconsfeed_id PKbiginticon_id PKbigintusersuser_iduser_idcategory_idfeed_idicon_id

Relationships

categories

4 cols · no RLS · line 29View in 3D
ColumnTypeNullDefaultReferencesNotes
id PKserialNOT NULL
user_id integerNOT NULLusers.id 1:N · CASCADE
title textNOT NULL
hide_globally booleannull

Indexes

  • none

Referenced by

feeds

42 cols · no RLS · line 38View in 3D
  • Foreign key without index category_id — PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on categories must scan feeds to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own. An index is not free. Every write to feeds maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often categories rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
  • Wide table (42 columns) — Tables this wide usually hide several entities (or a JSON column that wants to be one). Every row update rewrites the whole tuple; TOAST kicks in; indexes bloat.
ColumnTypeNullDefaultReferencesNotes
id PKbigserialNOT NULL
user_id integerNOT NULLusers.id 1:N · CASCADE
category_id integerNOT NULLcategories.id 1:N · CASCADE
title textNOT NULL
feed_url textNOT NULL
site_url textNOT NULL
checked_at timestamptznullnow()
etag_header textnull''
last_modified_header textnull''
parsing_error_msg textnull''
parsing_error_count integernull0
scraper_rules textnull
rewrite_rules textnull
crawler booleannull
username textnull
password textnull
user_agent textnull
disabled booleannull
next_check_at timestamptznull
ignore_http_cache boolnull
fetch_via_proxy boolnull
blocklist_rules textnull
keeplist_rules textnull
allow_self_signed_certificates booleannull
cookie textnull
hide_globally booleannull
url_rewrite_rules textnull
no_media_player booleannull
apprise_service_urls textnull
disable_http2 boolnull
description textnull
ntfy_enabled boolnull
ntfy_priority integernull
webhook_url textnull
pushover_enabled boolnull
pushover_priority integernull
ntfy_topic textnull
proxy_url textnull
block_filter_entry_rules textnull
keep_filter_entry_rules textnull
ignore_entry_updates boolnull
language textnull

Indexes

  • (user_id, category_id) feeds_user_category_idx

Referenced by

icons

5 cols · no RLS · line 89View in 3D
  • Enum-like column with no CHECK mime_type — mime_type is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.
ColumnTypeNullDefaultReferencesNotes
id PKbigserialNOT NULL
hash UKtextNOT NULL
mime_type textNOT NULL
content byteaNOT NULL
external_id textnull

Indexes

  • UNIQUE (external_id) WHERE external_id <> '' icons_external_id_idx

Referenced by

feed_icons

2 cols · no RLS · line 97View in 3D
  • Foreign key without index icon_id — PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on icons must scan feed_icons to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own. An index is not free. Every write to feed_icons maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often icons rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
ColumnTypeNullDefaultReferencesNotes
feed_id PKbigintNOT NULLfeeds.id 1:N · CASCADE
icon_id PKbigintNOT NULLicons.id 1:N · CASCADE

Indexes

  • none

Referenced by

  • nothing

Entries 3 tables

Fetched entries, their enclosures, and the tombstones that keep a deleted entry from coming back.

Entity-relationship diagram: entries, enclosures, entry_tombstonesentriesid PKbigserialuser_id FKintegerfeed_id FKbiginthashtextpublished_attimestamptztitletexturltextauthortextcontenttextstatusentry_statusstarredboolcomments_urltextdocument_vectorstsvectorchanged_attimestamptzshare_codetextreading_timeintegercreated_attimestamptztagstext[]languagetextenclosuresid PKbigserialuser_id FKintegerentry_id FKbiginturltextsizeintegermime_typetextmedia_progressionintegerentry_tombstonesfeed_id PKbiginthash PKtextdeleted_attimestamptzfeedsusersuser_idfeed_idfeed_iduser_identry_id

Relationships

entries

19 cols · no RLS · line 58View in 3D
ColumnTypeNullDefaultReferencesNotes
id PKbigserialNOT NULL
user_id integerNOT NULLusers.id 1:N · CASCADE
feed_id bigintNOT NULLfeeds.id 1:N · CASCADE
hash textNOT NULL
published_at timestamptzNOT NULL
title textNOT NULL
url textNOT NULL
author textnull
content textnull
status entry_statusnull'unread'
starred boolnull
comments_url textnull
document_vectors tsvectornull
changed_at timestamptznull
share_code textnull
reading_time integernull
created_at timestamptznull
tags text[]null
language textnull

Indexes

  • (feed_id) entries_feed_idx
  • (user_id, status) entries_user_status_idx
  • (document_vectors) document_vectors_idx
  • UNIQUE (share_code) WHERE share_code <> '' entries_share_code_idx
  • (user_id, feed_id) entries_user_feed_idx
  • (id, user_id, status) entries_id_user_status_idx
  • (feed_id, status, hash) entries_feed_id_status_hash_idx
  • (user_id, status, starred) entries_user_id_status_starred_idx
  • (feed_id, url) WHERE length(url) < 2000 entries_feed_url_idx
  • (user_id, status, feed_id) entries_user_status_feed_idx
  • (user_id, status, changed_at) entries_user_status_changed_idx
  • (document_vectors) WHERE status <> 'removed' document_vectors_idx
  • (document_vectors) document_vectors_idx

Referenced by

enclosures

7 cols · no RLS · line 77View in 3D
  • Foreign key without index entry_id — PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on entries must scan enclosures to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own. An index is not free. Every write to enclosures maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often entries rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
  • Enum-like column with no CHECK mime_type — mime_type is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.
ColumnTypeNullDefaultReferencesNotes
id PKbigserialNOT NULL
user_id integerNOT NULLusers.id 1:N · CASCADE
entry_id bigintNOT NULLentries.id 1:N · CASCADE
url textNOT NULL
size integernull0
mime_type textnull''
media_progression integernull

Indexes

  • (user_id, entry_id, encode(sha256(CAST(url AS bytea)), 'hex')) enclosures_user_entry_url_idx
  • UNIQUE (user_id, entry_id, encode(sha256(CAST(url AS bytea)), 'hex')) enclosures_user_entry_url_unique_idx
  • UNIQUE (user_id, entry_id, encode(sha256(CAST(url AS bytea)), 'hex')) enclosures_user_entry_url_unique_idx

Referenced by

  • nothing

entry_tombstones

3 cols · no RLS · line 614View in 3D
ColumnTypeNullDefaultReferencesNotes
feed_id PKbigintNOT NULLfeeds.id 1:N · CASCADE
hash PKtextNOT NULLCHECK hash <> ''
deleted_at timestamptzNOT NULLnow()

Indexes

  • (deleted_at) entry_tombstones_deleted_at_idx

Referenced by

  • nothing

System 3 tables

Schema version, CSRF tokens, and the ACME certificate cache.

Entity-relationship diagram: schema_version, tokens, acme_cacheschema_versionversiontexttokensid PKtextvalue PKtextcreated_attimestamptzacme_cachekey PKvarchar(400)databyteaupdated_attimestamptz

Relationships

No foreign keys in this domain.

schema_version

1 cols · no RLS · line 1View in 3D
  • No primary key — Without a primary key rows are not individually addressable: no safe UPDATE/DELETE of one row, no logical replication, ORMs misbehave, and duplicates are legal.
  • Isolated table — schema_version references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
ColumnTypeNullDefaultReferencesNotes
version textNOT NULL

Indexes

  • none

Referenced by

  • nothing

tokens

3 cols · no RLS · line 105View in 3D
  • Isolated table — tokens references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
ColumnTypeNullDefaultReferencesNotes
id PKtextNOT NULL
value PKtextNOT NULL
created_at timestamptzNOT NULLnow()

Indexes

  • none

Referenced by

  • nothing

acme_cache

3 cols · no RLS · line 274View in 3D
  • Isolated table — acme_cache references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
ColumnTypeNullDefaultReferencesNotes
key PKvarchar(400)NOT NULL
data byteaNOT NULL
updated_at timestamptzNOT NULL

Indexes

  • none

Referenced by

  • nothing