Repository files navigation

PRKS — Personal Research Knowledge System

PRKS is a self-hosted web application for organizing research materials: PDFs, Markdown notes, and online video references. It stores everything in a SQLite database and on-disk files on your machine—no separate database server. The UI supports folders, tags, reading progress, people and bibliographic metadata, PDF annotations, and playlists for videos.

Folder of public-domain books

Origin of Species open in the PDF reader

People in the library

Requirements

  • Python 3.12+
  • PyMuPDF 1.24.10

The HTTP server and SQLite access use the Python standard library.

Quick start (local)

From the repository root:

pip install -r requirements.txt
python prks_app.py

The process listens on 127.0.0.1:8080 only. Open http://127.0.0.1:8080 in a browser. No extra firewall or network setup is required for this case.

Optional port (still loopback):

python prks_app.py --port 9000

To listen on every local interface (LAN or VPN), pass an explicit host:

python prks_app.py --host 0.0.0.0

--host localhost also works and binds that name. The default is the literal address 127.0.0.1, not localhost.

Testing mode (Creates seperate testing database)

python prks_app.py --testing

This sets PRKS_TESTING=1 and uses port 8070 by default (unless you pass --port). With PRKS_STORAGE unset it defaults to data_testing/ so repo data/ is untouched. You may set PRKS_STORAGE to an explicit safe testing root. Testing mode refuses /data and the repository data/ directory (and descendants), including via symlinks. prks_app.py is the only process entry.

Docker

Build:

Use ./docker-build.sh, which builds prks:latest and prunes dangling images (from previous builds).

And run with Compose (from the repo root):

docker compose up -d

The container process binds 0.0.0.0:8080 so Docker port forwarding can reach it. Compose then publishes that port on the host loopback only (127.0.0.1:8080:8080). Container 0.0.0.0 is not the same as exposing PRKS on every host interface.

Open http://127.0.0.1:8080 on the machine that runs Compose. This also sets PRKS_STORAGE=/data, mounts ./data on the host to /data in the container, and runs the process as ${UID:-1000}:${GID:-1000} so files on the bind mount match your user. The entrypoint creates /data/pdfs if needed and runs python /app/prks_app.py --host 0.0.0.0.

To publish the host port on every interface (LAN access):

PRKS_PUBLISH_HOST=0.0.0.0 docker compose up -d

PRKS has no application-level authentication. Use that override only on a network you already trust, or behind an access layer you control.

Configuration and data layout

VariablePurpose
PRKS_STORAGEIf set, root directory for persistent data. Database: $PRKS_STORAGE/prks_data.db. PDFs: $PRKS_STORAGE/pdfs/. Thumbnails: $PRKS_STORAGE/thumbs/.
PRKS_TESTINGWhen truthy (1, true, yes), uses testing paths and stricter checks (see testing mode above).
PRKS_THUMB_LOSSLESSWhen truthy, PDF card thumbnails use lossless WebP/PNG cache encoding (debugging). Default is card-optimized lossy WebP; cache filenames use rev _v2.
PRKS_LOG_LEVELStderr log level. Default INFO. Changes volume, not what kinds of data may be logged.
PRKS_LOG_FILE_LEVELPersistent file log level. Default ERROR.
PRKS_LOG_RETENTION_DAYSRotated persistent log copies to keep. Default 7.
PRKS_LOG_FILEOverride path for the rotating error log. Default $PRKS_STORAGE/prks-errors.log.

If PRKS_STORAGE is unset, non-testing runs use the project’s data/ directory: data/prks_data.db, data/pdfs/, data/thumbs/, and person portrait cache data/people/ (lossy WebP, max 512px edge, keyed by person id + image_url hash).

Person profile images (GET /api/persons/{id}/profile-image) are optional. image_url must be a direct public HTTP/HTTPS URL (HTTPS preferred) that itself returns HTTP 200. PRKS does not follow redirects, and private/local/link-local targets are refused. Only static JPEG/PNG/WebP/GIF rasters are accepted. The download is size- and time-bounded; the image is decoded and transcoded (max 512px edge, usually WebP) before anything is cached. Original remote bytes are not kept. Local portrait upload is not part of this feature. Updating a valid image_url clears that person’s cached portraits.

Backup your database by copying /data folder.

Logging and privacy

Persistent log: <storage>/prks-errors.log. Default persistent threshold is ERROR. Rotation is daily at midnight. Retention is 7 days.

PRKS logs describe operations and failures, not the contents of the research library. Increasing PRKS_LOG_LEVEL or PRKS_LOG_FILE_LEVEL (including DEBUG) changes volume, not privacy policy.

Logs may include:

  • event names
  • request IDs
  • safe endpoint templates (/api/search, /api/pdfs/:pdf, /api/works/:id)
  • HTTP status
  • internal opaque IDs (work_id, processing_file_id)
  • counts, page numbers, byte ranges, file sizes
  • exception class names
  • repository-relative traceback locations

PRKS deliberately does not log:

  • research titles, notes, abstracts, annotations, or selected PDF text
  • person, tag, folder, or publisher names
  • search terms
  • PDF filenames or absolute filesystem paths
  • source, portrait, or image URLs
  • request bodies, query strings, or headers (Host, Origin, User-Agent, …)
  • client/LAN IP addresses
  • browser messages, stacks, routes, or hash state
  • raw exception messages
  • qpdf stderr

Docker captures process stderr. Console output follows the same privacy rules as the persistent file.

There is no remote telemetry. POST /api/client-errors is same-application metadata for correlating browser failures with server request IDs.

Development and tests

python run_tests.py

This discovers tests under tests/. run_tests.py always forces PRKS_TESTING=1 and PRKS_STORAGE to the repo’s data_testing/ directory and clears PRKS_FOR_PROCESSING_DIR and PRKS_LOG_FILE. That is stricter than python prks_app.py --testing, which may honor an explicit safe PRKS_STORAGE. Neither path uses ./data or container /data.

Project layout

PathRole
prks_app.pyOnly process entry: parses --testing, --port, --host, starts the server.
backend/server.pyHTTP handler: static frontend, REST-style /api/... routes.
backend/storage/config.pyFrozen storage snapshot and env parser.
backend/storage/paths.pyPath derivation and testing-mode containment.
backend/db_manager.pySQLite access and business logic.
backend/db_schema.sqlSchema and FTS triggers.
frontend/Static SPA (HTML, CSS, JS), PWA assets.
data/Default production database and files (gitignored as appropriate).
data_testing/Test fixtures and isolated DB/PDFs for automated tests.
tests/unittest modules.

Security note

PRKS is a single-user app with no built-in authentication. Direct runs bind 127.0.0.1 by default. Docker Compose publishes the host port on 127.0.0.1 by default. Reaching it from another machine requires an explicit --host or PRKS_PUBLISH_HOST override. Do that only on a trusted network.

Local browser use through http://127.0.0.1:8080 or http://localhost:8080 works without extra Host configuration. LAN access by IP literal (after PRKS_PUBLISH_HOST=0.0.0.0) also needs no PRKS_TRUSTED_HOSTS setting.

Custom LAN DNS names must be listed exactly:

PRKS_PUBLISH_HOST=0.0.0.0 \
PRKS_TRUSTED_HOSTS=prks.home.arpa \
docker compose up -d

Malformed PRKS_TRUSTED_HOSTS entries refuse to start the server. This variable is for extra DNS hostnames on direct HTTP/LAN access, not reverse-proxy or HTTPS termination.

The HTTP adapter validates Host on every request, rejects cross-origin state-changing /api/ requests when Origin is supplied (Origin: null included), and requires application/json for JSON POST/PATCH bodies. Missing Origin remains allowed for local scripts and non-browser clients. PRKS does not send CORS headers and does not allow cross-origin API access.

These controls reduce accidental/cross-origin access and DNS-rebinding risk. They are not authentication. Public Internet exposure is still unsafe.

Research notes (works.text_content) are stored as raw Markdown. Preview HTML is produced by EasyMDE/Marked and then sanitized with a pinned local DOMPurify allowlist (frontend/vendor/dompurify, frontend/js/markdown-sanitize.js). Arbitrary or active HTML is not a supported contract: unsafe tags, attributes, and URL schemes are stripped from the preview only. Sanitization never rewrites saved Markdown.

Frontend libraries (Inter, EasyMDE, CodeMirror, Lucide, DOMPurify, the PDF viewer) are local files under frontend/vendor/. Node is not a runtime dependency. Docker does not run npm. To rebuild the PDF viewer after changing tools/pdf-viewer/:

cd tools/pdf-viewer
npm ci
npm run build

That writes frontend/vendor/prks-pdf-viewer/ (EmbedPDF 2.15.0 + React 18.3.1, bundled). React is not part of the PRKS UI; it exists only inside that file. The PDF fixture is served by the same test-only server as the sanitizer fixture: open the printed tests/browser/pdf_viewer.html URL. It must report PASS with no jsDelivr / Google Fonts / unpkg requests.

The sanitizer-boundary browser fixture is not served by the app. From the repo root:

python tests/browser/serve.py

Open the printed 127.0.0.1 URL (and the ?dompurify=absent / ?dompurify=unsupported variants). The fixture must report PASS. Do not use production data/ or a live PRKS_STORAGE tree for this check.

About

Personal Research Knowledge System

Resources

Stars

0 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

PRKS — Personal Research Knowledge System

PRKS is a self-hosted web application for organizing research materials: PDFs, Markdown notes, and online video references. It stores everything in a SQLite database and on-disk files on your machine—no separate database server. The UI supports folders, tags, reading progress, people and bibliographic metadata, PDF annotations, and playlists for videos.

Folder of public-domain books

Origin of Species open in the PDF reader

People in the library

Requirements

  • Python 3.12+
  • PyMuPDF 1.24.10

The HTTP server and SQLite access use the Python standard library.

Quick start (local)

From the repository root:

pip install -r requirements.txt
python prks_app.py

The process listens on 127.0.0.1:8080 only. Open http://127.0.0.1:8080 in a browser. No extra firewall or network setup is required for this case.

Optional port (still loopback):

python prks_app.py --port 9000

To listen on every local interface (LAN or VPN), pass an explicit host:

python prks_app.py --host 0.0.0.0

--host localhost also works and binds that name. The default is the literal address 127.0.0.1, not localhost.

Testing mode (Creates seperate testing database)

python prks_app.py --testing

This sets PRKS_TESTING=1 and uses port 8070 by default (unless you pass --port). With PRKS_STORAGE unset it defaults to data_testing/ so repo data/ is untouched. You may set PRKS_STORAGE to an explicit safe testing root. Testing mode refuses /data and the repository data/ directory (and descendants), including via symlinks. prks_app.py is the only process entry.

Docker

Build:

Use ./docker-build.sh, which builds prks:latest and prunes dangling images (from previous builds).

And run with Compose (from the repo root):

docker compose up -d

The container process binds 0.0.0.0:8080 so Docker port forwarding can reach it. Compose then publishes that port on the host loopback only (127.0.0.1:8080:8080). Container 0.0.0.0 is not the same as exposing PRKS on every host interface.

Open http://127.0.0.1:8080 on the machine that runs Compose. This also sets PRKS_STORAGE=/data, mounts ./data on the host to /data in the container, and runs the process as ${UID:-1000}:${GID:-1000} so files on the bind mount match your user. The entrypoint creates /data/pdfs if needed and runs python /app/prks_app.py --host 0.0.0.0.

To publish the host port on every interface (LAN access):

PRKS_PUBLISH_HOST=0.0.0.0 docker compose up -d

PRKS has no application-level authentication. Use that override only on a network you already trust, or behind an access layer you control.

Configuration and data layout

VariablePurpose
PRKS_STORAGEIf set, root directory for persistent data. Database: $PRKS_STORAGE/prks_data.db. PDFs: $PRKS_STORAGE/pdfs/. Thumbnails: $PRKS_STORAGE/thumbs/.
PRKS_TESTINGWhen truthy (1, true, yes), uses testing paths and stricter checks (see testing mode above).
PRKS_THUMB_LOSSLESSWhen truthy, PDF card thumbnails use lossless WebP/PNG cache encoding (debugging). Default is card-optimized lossy WebP; cache filenames use rev _v2.
PRKS_LOG_LEVELStderr log level. Default INFO. Changes volume, not what kinds of data may be logged.
PRKS_LOG_FILE_LEVELPersistent file log level. Default ERROR.
PRKS_LOG_RETENTION_DAYSRotated persistent log copies to keep. Default 7.
PRKS_LOG_FILEOverride path for the rotating error log. Default $PRKS_STORAGE/prks-errors.log.

If PRKS_STORAGE is unset, non-testing runs use the project’s data/ directory: data/prks_data.db, data/pdfs/, data/thumbs/, and person portrait cache data/people/ (lossy WebP, max 512px edge, keyed by person id + image_url hash).

Person profile images (GET /api/persons/{id}/profile-image) are optional. image_url must be a direct public HTTP/HTTPS URL (HTTPS preferred) that itself returns HTTP 200. PRKS does not follow redirects, and private/local/link-local targets are refused. Only static JPEG/PNG/WebP/GIF rasters are accepted. The download is size- and time-bounded; the image is decoded and transcoded (max 512px edge, usually WebP) before anything is cached. Original remote bytes are not kept. Local portrait upload is not part of this feature. Updating a valid image_url clears that person’s cached portraits.

Backup your database by copying /data folder.

Logging and privacy

Persistent log: <storage>/prks-errors.log. Default persistent threshold is ERROR. Rotation is daily at midnight. Retention is 7 days.

PRKS logs describe operations and failures, not the contents of the research library. Increasing PRKS_LOG_LEVEL or PRKS_LOG_FILE_LEVEL (including DEBUG) changes volume, not privacy policy.

Logs may include:

  • event names
  • request IDs
  • safe endpoint templates (/api/search, /api/pdfs/:pdf, /api/works/:id)
  • HTTP status
  • internal opaque IDs (work_id, processing_file_id)
  • counts, page numbers, byte ranges, file sizes
  • exception class names
  • repository-relative traceback locations

PRKS deliberately does not log:

  • research titles, notes, abstracts, annotations, or selected PDF text
  • person, tag, folder, or publisher names
  • search terms
  • PDF filenames or absolute filesystem paths
  • source, portrait, or image URLs
  • request bodies, query strings, or headers (Host, Origin, User-Agent, …)
  • client/LAN IP addresses
  • browser messages, stacks, routes, or hash state
  • raw exception messages
  • qpdf stderr

Docker captures process stderr. Console output follows the same privacy rules as the persistent file.

There is no remote telemetry. POST /api/client-errors is same-application metadata for correlating browser failures with server request IDs.

Development and tests

python run_tests.py

This discovers tests under tests/. run_tests.py always forces PRKS_TESTING=1 and PRKS_STORAGE to the repo’s data_testing/ directory and clears PRKS_FOR_PROCESSING_DIR and PRKS_LOG_FILE. That is stricter than python prks_app.py --testing, which may honor an explicit safe PRKS_STORAGE. Neither path uses ./data or container /data.

Project layout

PathRole
prks_app.pyOnly process entry: parses --testing, --port, --host, starts the server.
backend/server.pyHTTP handler: static frontend, REST-style /api/... routes.
backend/storage/config.pyFrozen storage snapshot and env parser.
backend/storage/paths.pyPath derivation and testing-mode containment.
backend/db_manager.pySQLite access and business logic.
backend/db_schema.sqlSchema and FTS triggers.
frontend/Static SPA (HTML, CSS, JS), PWA assets.
data/Default production database and files (gitignored as appropriate).
data_testing/Test fixtures and isolated DB/PDFs for automated tests.
tests/unittest modules.

Security note

PRKS is a single-user app with no built-in authentication. Direct runs bind 127.0.0.1 by default. Docker Compose publishes the host port on 127.0.0.1 by default. Reaching it from another machine requires an explicit --host or PRKS_PUBLISH_HOST override. Do that only on a trusted network.

Local browser use through http://127.0.0.1:8080 or http://localhost:8080 works without extra Host configuration. LAN access by IP literal (after PRKS_PUBLISH_HOST=0.0.0.0) also needs no PRKS_TRUSTED_HOSTS setting.

Custom LAN DNS names must be listed exactly:

PRKS_PUBLISH_HOST=0.0.0.0 \
PRKS_TRUSTED_HOSTS=prks.home.arpa \
docker compose up -d

Malformed PRKS_TRUSTED_HOSTS entries refuse to start the server. This variable is for extra DNS hostnames on direct HTTP/LAN access, not reverse-proxy or HTTPS termination.

The HTTP adapter validates Host on every request, rejects cross-origin state-changing /api/ requests when Origin is supplied (Origin: null included), and requires application/json for JSON POST/PATCH bodies. Missing Origin remains allowed for local scripts and non-browser clients. PRKS does not send CORS headers and does not allow cross-origin API access.

These controls reduce accidental/cross-origin access and DNS-rebinding risk. They are not authentication. Public Internet exposure is still unsafe.

Research notes (works.text_content) are stored as raw Markdown. Preview HTML is produced by EasyMDE/Marked and then sanitized with a pinned local DOMPurify allowlist (frontend/vendor/dompurify, frontend/js/markdown-sanitize.js). Arbitrary or active HTML is not a supported contract: unsafe tags, attributes, and URL schemes are stripped from the preview only. Sanitization never rewrites saved Markdown.

Frontend libraries (Inter, EasyMDE, CodeMirror, Lucide, DOMPurify, the PDF viewer) are local files under frontend/vendor/. Node is not a runtime dependency. Docker does not run npm. To rebuild the PDF viewer after changing tools/pdf-viewer/:

cd tools/pdf-viewer
npm ci
npm run build

That writes frontend/vendor/prks-pdf-viewer/ (EmbedPDF 2.15.0 + React 18.3.1, bundled). React is not part of the PRKS UI; it exists only inside that file. The PDF fixture is served by the same test-only server as the sanitizer fixture: open the printed tests/browser/pdf_viewer.html URL. It must report PASS with no jsDelivr / Google Fonts / unpkg requests.

The sanitizer-boundary browser fixture is not served by the app. From the repo root:

python tests/browser/serve.py

Open the printed 127.0.0.1 URL (and the ?dompurify=absent / ?dompurify=unsupported variants). The fixture must report PASS. Do not use production data/ or a live PRKS_STORAGE tree for this check.

About

Personal Research Knowledge System

Resources

Stars

0 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PRKS — Personal Research Knowledge System

PRKS is a self-hosted web application for organizing research materials: PDFs, Markdown notes, and online video references. It stores everything in a SQLite database and on-disk files on your machine—no separate database server. The UI supports folders, tags, reading progress, people and bibliographic metadata, PDF annotations, and playlists for videos.

Folder of public-domain books

Origin of Species open in the PDF reader

People in the library

Requirements

  • Python 3.12+
  • PyMuPDF 1.24.10

The HTTP server and SQLite access use the Python standard library.

Quick start (local)

From the repository root:

pip install -r requirements.txt
python prks_app.py

The process listens on 127.0.0.1:8080 only. Open http://127.0.0.1:8080 in a browser. No extra firewall or network setup is required for this case.

Optional port (still loopback):

python prks_app.py --port 9000

To listen on every local interface (LAN or VPN), pass an explicit host:

python prks_app.py --host 0.0.0.0

--host localhost also works and binds that name. The default is the literal address 127.0.0.1, not localhost.

Testing mode (Creates seperate testing database)

python prks_app.py --testing

This sets PRKS_TESTING=1 and uses port 8070 by default (unless you pass --port). With PRKS_STORAGE unset it defaults to data_testing/ so repo data/ is untouched. You may set PRKS_STORAGE to an explicit safe testing root. Testing mode refuses /data and the repository data/ directory (and descendants), including via symlinks. prks_app.py is the only process entry.

Docker

Build:

Use ./docker-build.sh, which builds prks:latest and prunes dangling images (from previous builds).

And run with Compose (from the repo root):

docker compose up -d

The container process binds 0.0.0.0:8080 so Docker port forwarding can reach it. Compose then publishes that port on the host loopback only (127.0.0.1:8080:8080). Container 0.0.0.0 is not the same as exposing PRKS on every host interface.

Open http://127.0.0.1:8080 on the machine that runs Compose. This also sets PRKS_STORAGE=/data, mounts ./data on the host to /data in the container, and runs the process as ${UID:-1000}:${GID:-1000} so files on the bind mount match your user. The entrypoint creates /data/pdfs if needed and runs python /app/prks_app.py --host 0.0.0.0.

To publish the host port on every interface (LAN access):

PRKS_PUBLISH_HOST=0.0.0.0 docker compose up -d

PRKS has no application-level authentication. Use that override only on a network you already trust, or behind an access layer you control.

Configuration and data layout

VariablePurpose
PRKS_STORAGEIf set, root directory for persistent data. Database: $PRKS_STORAGE/prks_data.db. PDFs: $PRKS_STORAGE/pdfs/. Thumbnails: $PRKS_STORAGE/thumbs/.
PRKS_TESTINGWhen truthy (1, true, yes), uses testing paths and stricter checks (see testing mode above).
PRKS_THUMB_LOSSLESSWhen truthy, PDF card thumbnails use lossless WebP/PNG cache encoding (debugging). Default is card-optimized lossy WebP; cache filenames use rev _v2.
PRKS_LOG_LEVELStderr log level. Default INFO. Changes volume, not what kinds of data may be logged.
PRKS_LOG_FILE_LEVELPersistent file log level. Default ERROR.
PRKS_LOG_RETENTION_DAYSRotated persistent log copies to keep. Default 7.
PRKS_LOG_FILEOverride path for the rotating error log. Default $PRKS_STORAGE/prks-errors.log.

If PRKS_STORAGE is unset, non-testing runs use the project’s data/ directory: data/prks_data.db, data/pdfs/, data/thumbs/, and person portrait cache data/people/ (lossy WebP, max 512px edge, keyed by person id + image_url hash).

Person profile images (GET /api/persons/{id}/profile-image) are optional. image_url must be a direct public HTTP/HTTPS URL (HTTPS preferred) that itself returns HTTP 200. PRKS does not follow redirects, and private/local/link-local targets are refused. Only static JPEG/PNG/WebP/GIF rasters are accepted. The download is size- and time-bounded; the image is decoded and transcoded (max 512px edge, usually WebP) before anything is cached. Original remote bytes are not kept. Local portrait upload is not part of this feature. Updating a valid image_url clears that person’s cached portraits.

Backup your database by copying /data folder.

Logging and privacy

Persistent log: <storage>/prks-errors.log. Default persistent threshold is ERROR. Rotation is daily at midnight. Retention is 7 days.

PRKS logs describe operations and failures, not the contents of the research library. Increasing PRKS_LOG_LEVEL or PRKS_LOG_FILE_LEVEL (including DEBUG) changes volume, not privacy policy.

Logs may include:

  • event names
  • request IDs
  • safe endpoint templates (/api/search, /api/pdfs/:pdf, /api/works/:id)
  • HTTP status
  • internal opaque IDs (work_id, processing_file_id)
  • counts, page numbers, byte ranges, file sizes
  • exception class names
  • repository-relative traceback locations

PRKS deliberately does not log:

  • research titles, notes, abstracts, annotations, or selected PDF text
  • person, tag, folder, or publisher names
  • search terms
  • PDF filenames or absolute filesystem paths
  • source, portrait, or image URLs
  • request bodies, query strings, or headers (Host, Origin, User-Agent, …)
  • client/LAN IP addresses
  • browser messages, stacks, routes, or hash state
  • raw exception messages
  • qpdf stderr

Docker captures process stderr. Console output follows the same privacy rules as the persistent file.

There is no remote telemetry. POST /api/client-errors is same-application metadata for correlating browser failures with server request IDs.

Development and tests

python run_tests.py

This discovers tests under tests/. run_tests.py always forces PRKS_TESTING=1 and PRKS_STORAGE to the repo’s data_testing/ directory and clears PRKS_FOR_PROCESSING_DIR and PRKS_LOG_FILE. That is stricter than python prks_app.py --testing, which may honor an explicit safe PRKS_STORAGE. Neither path uses ./data or container /data.

Project layout

PathRole
prks_app.pyOnly process entry: parses --testing, --port, --host, starts the server.
backend/server.pyHTTP handler: static frontend, REST-style /api/... routes.
backend/storage/config.pyFrozen storage snapshot and env parser.
backend/storage/paths.pyPath derivation and testing-mode containment.
backend/db_manager.pySQLite access and business logic.
backend/db_schema.sqlSchema and FTS triggers.
frontend/Static SPA (HTML, CSS, JS), PWA assets.
data/Default production database and files (gitignored as appropriate).
data_testing/Test fixtures and isolated DB/PDFs for automated tests.
tests/unittest modules.

Security note

PRKS is a single-user app with no built-in authentication. Direct runs bind 127.0.0.1 by default. Docker Compose publishes the host port on 127.0.0.1 by default. Reaching it from another machine requires an explicit --host or PRKS_PUBLISH_HOST override. Do that only on a trusted network.

Local browser use through http://127.0.0.1:8080 or http://localhost:8080 works without extra Host configuration. LAN access by IP literal (after PRKS_PUBLISH_HOST=0.0.0.0) also needs no PRKS_TRUSTED_HOSTS setting.

Custom LAN DNS names must be listed exactly:

PRKS_PUBLISH_HOST=0.0.0.0 \
PRKS_TRUSTED_HOSTS=prks.home.arpa \
docker compose up -d

Malformed PRKS_TRUSTED_HOSTS entries refuse to start the server. This variable is for extra DNS hostnames on direct HTTP/LAN access, not reverse-proxy or HTTPS termination.

The HTTP adapter validates Host on every request, rejects cross-origin state-changing /api/ requests when Origin is supplied (Origin: null included), and requires application/json for JSON POST/PATCH bodies. Missing Origin remains allowed for local scripts and non-browser clients. PRKS does not send CORS headers and does not allow cross-origin API access.

These controls reduce accidental/cross-origin access and DNS-rebinding risk. They are not authentication. Public Internet exposure is still unsafe.

Research notes (works.text_content) are stored as raw Markdown. Preview HTML is produced by EasyMDE/Marked and then sanitized with a pinned local DOMPurify allowlist (frontend/vendor/dompurify, frontend/js/markdown-sanitize.js). Arbitrary or active HTML is not a supported contract: unsafe tags, attributes, and URL schemes are stripped from the preview only. Sanitization never rewrites saved Markdown.

Frontend libraries (Inter, EasyMDE, CodeMirror, Lucide, DOMPurify, the PDF viewer) are local files under frontend/vendor/. Node is not a runtime dependency. Docker does not run npm. To rebuild the PDF viewer after changing tools/pdf-viewer/:

cd tools/pdf-viewer
npm ci
npm run build

That writes frontend/vendor/prks-pdf-viewer/ (EmbedPDF 2.15.0 + React 18.3.1, bundled). React is not part of the PRKS UI; it exists only inside that file. The PDF fixture is served by the same test-only server as the sanitizer fixture: open the printed tests/browser/pdf_viewer.html URL. It must report PASS with no jsDelivr / Google Fonts / unpkg requests.

The sanitizer-boundary browser fixture is not served by the app. From the repo root:

python tests/browser/serve.py

Open the printed 127.0.0.1 URL (and the ?dompurify=absent / ?dompurify=unsupported variants). The fixture must report PASS. Do not use production data/ or a live PRKS_STORAGE tree for this check.

About

Personal Research Knowledge System

Resources

Stars

0 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PRKS — Personal Research Knowledge System

PRKS is a self-hosted web application for organizing research materials: PDFs, Markdown notes, and online video references. It stores everything in a SQLite database and on-disk files on your machine—no separate database server. The UI supports folders, tags, reading progress, people and bibliographic metadata, PDF annotations, and playlists for videos.

Folder of public-domain books

Origin of Species open in the PDF reader

People in the library

Requirements

  • Python 3.12+
  • PyMuPDF 1.24.10

The HTTP server and SQLite access use the Python standard library.

Quick start (local)

From the repository root:

pip install -r requirements.txt
python prks_app.py

The process listens on 127.0.0.1:8080 only. Open http://127.0.0.1:8080 in a browser. No extra firewall or network setup is required for this case.

Optional port (still loopback):

python prks_app.py --port 9000

To listen on every local interface (LAN or VPN), pass an explicit host:

python prks_app.py --host 0.0.0.0

--host localhost also works and binds that name. The default is the literal address 127.0.0.1, not localhost.

Testing mode (Creates seperate testing database)

python prks_app.py --testing

This sets PRKS_TESTING=1 and uses port 8070 by default (unless you pass --port). With PRKS_STORAGE unset it defaults to data_testing/ so repo data/ is untouched. You may set PRKS_STORAGE to an explicit safe testing root. Testing mode refuses /data and the repository data/ directory (and descendants), including via symlinks. prks_app.py is the only process entry.

Docker

Build:

Use ./docker-build.sh, which builds prks:latest and prunes dangling images (from previous builds).

And run with Compose (from the repo root):

docker compose up -d

The container process binds 0.0.0.0:8080 so Docker port forwarding can reach it. Compose then publishes that port on the host loopback only (127.0.0.1:8080:8080). Container 0.0.0.0 is not the same as exposing PRKS on every host interface.

Open http://127.0.0.1:8080 on the machine that runs Compose. This also sets PRKS_STORAGE=/data, mounts ./data on the host to /data in the container, and runs the process as ${UID:-1000}:${GID:-1000} so files on the bind mount match your user. The entrypoint creates /data/pdfs if needed and runs python /app/prks_app.py --host 0.0.0.0.

To publish the host port on every interface (LAN access):

PRKS_PUBLISH_HOST=0.0.0.0 docker compose up -d

PRKS has no application-level authentication. Use that override only on a network you already trust, or behind an access layer you control.

Configuration and data layout

VariablePurpose
PRKS_STORAGEIf set, root directory for persistent data. Database: $PRKS_STORAGE/prks_data.db. PDFs: $PRKS_STORAGE/pdfs/. Thumbnails: $PRKS_STORAGE/thumbs/.
PRKS_TESTINGWhen truthy (1, true, yes), uses testing paths and stricter checks (see testing mode above).
PRKS_THUMB_LOSSLESSWhen truthy, PDF card thumbnails use lossless WebP/PNG cache encoding (debugging). Default is card-optimized lossy WebP; cache filenames use rev _v2.
PRKS_LOG_LEVELStderr log level. Default INFO. Changes volume, not what kinds of data may be logged.
PRKS_LOG_FILE_LEVELPersistent file log level. Default ERROR.
PRKS_LOG_RETENTION_DAYSRotated persistent log copies to keep. Default 7.
PRKS_LOG_FILEOverride path for the rotating error log. Default $PRKS_STORAGE/prks-errors.log.

If PRKS_STORAGE is unset, non-testing runs use the project’s data/ directory: data/prks_data.db, data/pdfs/, data/thumbs/, and person portrait cache data/people/ (lossy WebP, max 512px edge, keyed by person id + image_url hash).

Person profile images (GET /api/persons/{id}/profile-image) are optional. image_url must be a direct public HTTP/HTTPS URL (HTTPS preferred) that itself returns HTTP 200. PRKS does not follow redirects, and private/local/link-local targets are refused. Only static JPEG/PNG/WebP/GIF rasters are accepted. The download is size- and time-bounded; the image is decoded and transcoded (max 512px edge, usually WebP) before anything is cached. Original remote bytes are not kept. Local portrait upload is not part of this feature. Updating a valid image_url clears that person’s cached portraits.

Backup your database by copying /data folder.

Logging and privacy

Persistent log: <storage>/prks-errors.log. Default persistent threshold is ERROR. Rotation is daily at midnight. Retention is 7 days.

PRKS logs describe operations and failures, not the contents of the research library. Increasing PRKS_LOG_LEVEL or PRKS_LOG_FILE_LEVEL (including DEBUG) changes volume, not privacy policy.

Logs may include:

  • event names
  • request IDs
  • safe endpoint templates (/api/search, /api/pdfs/:pdf, /api/works/:id)
  • HTTP status
  • internal opaque IDs (work_id, processing_file_id)
  • counts, page numbers, byte ranges, file sizes
  • exception class names
  • repository-relative traceback locations

PRKS deliberately does not log:

  • research titles, notes, abstracts, annotations, or selected PDF text
  • person, tag, folder, or publisher names
  • search terms
  • PDF filenames or absolute filesystem paths
  • source, portrait, or image URLs
  • request bodies, query strings, or headers (Host, Origin, User-Agent, …)
  • client/LAN IP addresses
  • browser messages, stacks, routes, or hash state
  • raw exception messages
  • qpdf stderr

Docker captures process stderr. Console output follows the same privacy rules as the persistent file.

There is no remote telemetry. POST /api/client-errors is same-application metadata for correlating browser failures with server request IDs.

Development and tests

python run_tests.py

This discovers tests under tests/. run_tests.py always forces PRKS_TESTING=1 and PRKS_STORAGE to the repo’s data_testing/ directory and clears PRKS_FOR_PROCESSING_DIR and PRKS_LOG_FILE. That is stricter than python prks_app.py --testing, which may honor an explicit safe PRKS_STORAGE. Neither path uses ./data or container /data.

Project layout

PathRole
prks_app.pyOnly process entry: parses --testing, --port, --host, starts the server.
backend/server.pyHTTP handler: static frontend, REST-style /api/... routes.
backend/storage/config.pyFrozen storage snapshot and env parser.
backend/storage/paths.pyPath derivation and testing-mode containment.
backend/db_manager.pySQLite access and business logic.
backend/db_schema.sqlSchema and FTS triggers.
frontend/Static SPA (HTML, CSS, JS), PWA assets.
data/Default production database and files (gitignored as appropriate).
data_testing/Test fixtures and isolated DB/PDFs for automated tests.
tests/unittest modules.

Security note

PRKS is a single-user app with no built-in authentication. Direct runs bind 127.0.0.1 by default. Docker Compose publishes the host port on 127.0.0.1 by default. Reaching it from another machine requires an explicit --host or PRKS_PUBLISH_HOST override. Do that only on a trusted network.

Local browser use through http://127.0.0.1:8080 or http://localhost:8080 works without extra Host configuration. LAN access by IP literal (after PRKS_PUBLISH_HOST=0.0.0.0) also needs no PRKS_TRUSTED_HOSTS setting.

Custom LAN DNS names must be listed exactly:

PRKS_PUBLISH_HOST=0.0.0.0 \
PRKS_TRUSTED_HOSTS=prks.home.arpa \
docker compose up -d

Malformed PRKS_TRUSTED_HOSTS entries refuse to start the server. This variable is for extra DNS hostnames on direct HTTP/LAN access, not reverse-proxy or HTTPS termination.

The HTTP adapter validates Host on every request, rejects cross-origin state-changing /api/ requests when Origin is supplied (Origin: null included), and requires application/json for JSON POST/PATCH bodies. Missing Origin remains allowed for local scripts and non-browser clients. PRKS does not send CORS headers and does not allow cross-origin API access.

These controls reduce accidental/cross-origin access and DNS-rebinding risk. They are not authentication. Public Internet exposure is still unsafe.

Research notes (works.text_content) are stored as raw Markdown. Preview HTML is produced by EasyMDE/Marked and then sanitized with a pinned local DOMPurify allowlist (frontend/vendor/dompurify, frontend/js/markdown-sanitize.js). Arbitrary or active HTML is not a supported contract: unsafe tags, attributes, and URL schemes are stripped from the preview only. Sanitization never rewrites saved Markdown.

Frontend libraries (Inter, EasyMDE, CodeMirror, Lucide, DOMPurify, the PDF viewer) are local files under frontend/vendor/. Node is not a runtime dependency. Docker does not run npm. To rebuild the PDF viewer after changing tools/pdf-viewer/:

cd tools/pdf-viewer
npm ci
npm run build

That writes frontend/vendor/prks-pdf-viewer/ (EmbedPDF 2.15.0 + React 18.3.1, bundled). React is not part of the PRKS UI; it exists only inside that file. The PDF fixture is served by the same test-only server as the sanitizer fixture: open the printed tests/browser/pdf_viewer.html URL. It must report PASS with no jsDelivr / Google Fonts / unpkg requests.

The sanitizer-boundary browser fixture is not served by the app. From the repo root:

python tests/browser/serve.py

Open the printed 127.0.0.1 URL (and the ?dompurify=absent / ?dompurify=unsupported variants). The fixture must report PASS. Do not use production data/ or a live PRKS_STORAGE tree for this check.

About

Personal Research Knowledge System

Resources

Stars

0 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

PRKS — Personal Research Knowledge System

PRKS is a self-hosted web application for organizing research materials: PDFs, Markdown notes, and online video references. It stores everything in a SQLite database and on-disk files on your machine—no separate database server. The UI supports folders, tags, reading progress, people and bibliographic metadata, PDF annotations, and playlists for videos.

Folder of public-domain books

Origin of Species open in the PDF reader

People in the library

Requirements

  • Python 3.12+
  • PyMuPDF 1.24.10

The HTTP server and SQLite access use the Python standard library.

Quick start (local)

From the repository root:

pip install -r requirements.txt
python prks_app.py

The process listens on 127.0.0.1:8080 only. Open http://127.0.0.1:8080 in a browser. No extra firewall or network setup is required for this case.

Optional port (still loopback):

python prks_app.py --port 9000

To listen on every local interface (LAN or VPN), pass an explicit host:

python prks_app.py --host 0.0.0.0

--host localhost also works and binds that name. The default is the literal address 127.0.0.1, not localhost.

Testing mode (Creates seperate testing database)

python prks_app.py --testing

This sets PRKS_TESTING=1 and uses port 8070 by default (unless you pass --port). With PRKS_STORAGE unset it defaults to data_testing/ so repo data/ is untouched. You may set PRKS_STORAGE to an explicit safe testing root. Testing mode refuses /data and the repository data/ directory (and descendants), including via symlinks. prks_app.py is the only process entry.

Docker

Build:

Use ./docker-build.sh, which builds prks:latest and prunes dangling images (from previous builds).

And run with Compose (from the repo root):

docker compose up -d

The container process binds 0.0.0.0:8080 so Docker port forwarding can reach it. Compose then publishes that port on the host loopback only (127.0.0.1:8080:8080). Container 0.0.0.0 is not the same as exposing PRKS on every host interface.

Open http://127.0.0.1:8080 on the machine that runs Compose. This also sets PRKS_STORAGE=/data, mounts ./data on the host to /data in the container, and runs the process as ${UID:-1000}:${GID:-1000} so files on the bind mount match your user. The entrypoint creates /data/pdfs if needed and runs python /app/prks_app.py --host 0.0.0.0.

To publish the host port on every interface (LAN access):

PRKS_PUBLISH_HOST=0.0.0.0 docker compose up -d

PRKS has no application-level authentication. Use that override only on a network you already trust, or behind an access layer you control.

Configuration and data layout

VariablePurpose
PRKS_STORAGEIf set, root directory for persistent data. Database: $PRKS_STORAGE/prks_data.db. PDFs: $PRKS_STORAGE/pdfs/. Thumbnails: $PRKS_STORAGE/thumbs/.
PRKS_TESTINGWhen truthy (1, true, yes), uses testing paths and stricter checks (see testing mode above).
PRKS_THUMB_LOSSLESSWhen truthy, PDF card thumbnails use lossless WebP/PNG cache encoding (debugging). Default is card-optimized lossy WebP; cache filenames use rev _v2.
PRKS_LOG_LEVELStderr log level. Default INFO. Changes volume, not what kinds of data may be logged.
PRKS_LOG_FILE_LEVELPersistent file log level. Default ERROR.
PRKS_LOG_RETENTION_DAYSRotated persistent log copies to keep. Default 7.
PRKS_LOG_FILEOverride path for the rotating error log. Default $PRKS_STORAGE/prks-errors.log.

If PRKS_STORAGE is unset, non-testing runs use the project’s data/ directory: data/prks_data.db, data/pdfs/, data/thumbs/, and person portrait cache data/people/ (lossy WebP, max 512px edge, keyed by person id + image_url hash).

Person profile images (GET /api/persons/{id}/profile-image) are optional. image_url must be a direct public HTTP/HTTPS URL (HTTPS preferred) that itself returns HTTP 200. PRKS does not follow redirects, and private/local/link-local targets are refused. Only static JPEG/PNG/WebP/GIF rasters are accepted. The download is size- and time-bounded; the image is decoded and transcoded (max 512px edge, usually WebP) before anything is cached. Original remote bytes are not kept. Local portrait upload is not part of this feature. Updating a valid image_url clears that person’s cached portraits.

Backup your database by copying /data folder.

Logging and privacy

Persistent log: <storage>/prks-errors.log. Default persistent threshold is ERROR. Rotation is daily at midnight. Retention is 7 days.

PRKS logs describe operations and failures, not the contents of the research library. Increasing PRKS_LOG_LEVEL or PRKS_LOG_FILE_LEVEL (including DEBUG) changes volume, not privacy policy.

Logs may include:

  • event names
  • request IDs
  • safe endpoint templates (/api/search, /api/pdfs/:pdf, /api/works/:id)
  • HTTP status
  • internal opaque IDs (work_id, processing_file_id)
  • counts, page numbers, byte ranges, file sizes
  • exception class names
  • repository-relative traceback locations

PRKS deliberately does not log:

  • research titles, notes, abstracts, annotations, or selected PDF text
  • person, tag, folder, or publisher names
  • search terms
  • PDF filenames or absolute filesystem paths
  • source, portrait, or image URLs
  • request bodies, query strings, or headers (Host, Origin, User-Agent, …)
  • client/LAN IP addresses
  • browser messages, stacks, routes, or hash state
  • raw exception messages
  • qpdf stderr

Docker captures process stderr. Console output follows the same privacy rules as the persistent file.

There is no remote telemetry. POST /api/client-errors is same-application metadata for correlating browser failures with server request IDs.

Development and tests

python run_tests.py

This discovers tests under tests/. run_tests.py always forces PRKS_TESTING=1 and PRKS_STORAGE to the repo’s data_testing/ directory and clears PRKS_FOR_PROCESSING_DIR and PRKS_LOG_FILE. That is stricter than python prks_app.py --testing, which may honor an explicit safe PRKS_STORAGE. Neither path uses ./data or container /data.

Project layout

PathRole
prks_app.pyOnly process entry: parses --testing, --port, --host, starts the server.
backend/server.pyHTTP handler: static frontend, REST-style /api/... routes.
backend/storage/config.pyFrozen storage snapshot and env parser.
backend/storage/paths.pyPath derivation and testing-mode containment.
backend/db_manager.pySQLite access and business logic.
backend/db_schema.sqlSchema and FTS triggers.
frontend/Static SPA (HTML, CSS, JS), PWA assets.
data/Default production database and files (gitignored as appropriate).
data_testing/Test fixtures and isolated DB/PDFs for automated tests.
tests/unittest modules.

Security note

PRKS is a single-user app with no built-in authentication. Direct runs bind 127.0.0.1 by default. Docker Compose publishes the host port on 127.0.0.1 by default. Reaching it from another machine requires an explicit --host or PRKS_PUBLISH_HOST override. Do that only on a trusted network.

Local browser use through http://127.0.0.1:8080 or http://localhost:8080 works without extra Host configuration. LAN access by IP literal (after PRKS_PUBLISH_HOST=0.0.0.0) also needs no PRKS_TRUSTED_HOSTS setting.

Custom LAN DNS names must be listed exactly:

PRKS_PUBLISH_HOST=0.0.0.0 \
PRKS_TRUSTED_HOSTS=prks.home.arpa \
docker compose up -d

Malformed PRKS_TRUSTED_HOSTS entries refuse to start the server. This variable is for extra DNS hostnames on direct HTTP/LAN access, not reverse-proxy or HTTPS termination.

The HTTP adapter validates Host on every request, rejects cross-origin state-changing /api/ requests when Origin is supplied (Origin: null included), and requires application/json for JSON POST/PATCH bodies. Missing Origin remains allowed for local scripts and non-browser clients. PRKS does not send CORS headers and does not allow cross-origin API access.

These controls reduce accidental/cross-origin access and DNS-rebinding risk. They are not authentication. Public Internet exposure is still unsafe.

Research notes (works.text_content) are stored as raw Markdown. Preview HTML is produced by EasyMDE/Marked and then sanitized with a pinned local DOMPurify allowlist (frontend/vendor/dompurify, frontend/js/markdown-sanitize.js). Arbitrary or active HTML is not a supported contract: unsafe tags, attributes, and URL schemes are stripped from the preview only. Sanitization never rewrites saved Markdown.

Frontend libraries (Inter, EasyMDE, CodeMirror, Lucide, DOMPurify, the PDF viewer) are local files under frontend/vendor/. Node is not a runtime dependency. Docker does not run npm. To rebuild the PDF viewer after changing tools/pdf-viewer/:

cd tools/pdf-viewer
npm ci
npm run build

That writes frontend/vendor/prks-pdf-viewer/ (EmbedPDF 2.15.0 + React 18.3.1, bundled). React is not part of the PRKS UI; it exists only inside that file. The PDF fixture is served by the same test-only server as the sanitizer fixture: open the printed tests/browser/pdf_viewer.html URL. It must report PASS with no jsDelivr / Google Fonts / unpkg requests.

The sanitizer-boundary browser fixture is not served by the app. From the repo root:

python tests/browser/serve.py

Open the printed 127.0.0.1 URL (and the ?dompurify=absent / ?dompurify=unsupported variants). The fixture must report PASS. Do not use production data/ or a live PRKS_STORAGE tree for this check.

About

Personal Research Knowledge System

Resources

Stars

0 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PRKS — Personal Research Knowledge System

PRKS is a self-hosted web application for organizing research materials: PDFs, Markdown notes, and online video references. It stores everything in a SQLite database and on-disk files on your machine—no separate database server. The UI supports folders, tags, reading progress, people and bibliographic metadata, PDF annotations, and playlists for videos.

Folder of public-domain books

Origin of Species open in the PDF reader

People in the library

Requirements

  • Python 3.12+
  • PyMuPDF 1.24.10

The HTTP server and SQLite access use the Python standard library.

Quick start (local)

From the repository root:

pip install -r requirements.txt
python prks_app.py

The process listens on 127.0.0.1:8080 only. Open http://127.0.0.1:8080 in a browser. No extra firewall or network setup is required for this case.

Optional port (still loopback):

python prks_app.py --port 9000

To listen on every local interface (LAN or VPN), pass an explicit host:

python prks_app.py --host 0.0.0.0

--host localhost also works and binds that name. The default is the literal address 127.0.0.1, not localhost.

Testing mode (Creates seperate testing database)

python prks_app.py --testing

This sets PRKS_TESTING=1 and uses port 8070 by default (unless you pass --port). With PRKS_STORAGE unset it defaults to data_testing/ so repo data/ is untouched. You may set PRKS_STORAGE to an explicit safe testing root. Testing mode refuses /data and the repository data/ directory (and descendants), including via symlinks. prks_app.py is the only process entry.

Docker

Build:

Use ./docker-build.sh, which builds prks:latest and prunes dangling images (from previous builds).

And run with Compose (from the repo root):

docker compose up -d

The container process binds 0.0.0.0:8080 so Docker port forwarding can reach it. Compose then publishes that port on the host loopback only (127.0.0.1:8080:8080). Container 0.0.0.0 is not the same as exposing PRKS on every host interface.

Open http://127.0.0.1:8080 on the machine that runs Compose. This also sets PRKS_STORAGE=/data, mounts ./data on the host to /data in the container, and runs the process as ${UID:-1000}:${GID:-1000} so files on the bind mount match your user. The entrypoint creates /data/pdfs if needed and runs python /app/prks_app.py --host 0.0.0.0.

To publish the host port on every interface (LAN access):

PRKS_PUBLISH_HOST=0.0.0.0 docker compose up -d

PRKS has no application-level authentication. Use that override only on a network you already trust, or behind an access layer you control.

Configuration and data layout

VariablePurpose
PRKS_STORAGEIf set, root directory for persistent data. Database: $PRKS_STORAGE/prks_data.db. PDFs: $PRKS_STORAGE/pdfs/. Thumbnails: $PRKS_STORAGE/thumbs/.
PRKS_TESTINGWhen truthy (1, true, yes), uses testing paths and stricter checks (see testing mode above).
PRKS_THUMB_LOSSLESSWhen truthy, PDF card thumbnails use lossless WebP/PNG cache encoding (debugging). Default is card-optimized lossy WebP; cache filenames use rev _v2.
PRKS_LOG_LEVELStderr log level. Default INFO. Changes volume, not what kinds of data may be logged.
PRKS_LOG_FILE_LEVELPersistent file log level. Default ERROR.
PRKS_LOG_RETENTION_DAYSRotated persistent log copies to keep. Default 7.
PRKS_LOG_FILEOverride path for the rotating error log. Default $PRKS_STORAGE/prks-errors.log.

If PRKS_STORAGE is unset, non-testing runs use the project’s data/ directory: data/prks_data.db, data/pdfs/, data/thumbs/, and person portrait cache data/people/ (lossy WebP, max 512px edge, keyed by person id + image_url hash).

Person profile images (GET /api/persons/{id}/profile-image) are optional. image_url must be a direct public HTTP/HTTPS URL (HTTPS preferred) that itself returns HTTP 200. PRKS does not follow redirects, and private/local/link-local targets are refused. Only static JPEG/PNG/WebP/GIF rasters are accepted. The download is size- and time-bounded; the image is decoded and transcoded (max 512px edge, usually WebP) before anything is cached. Original remote bytes are not kept. Local portrait upload is not part of this feature. Updating a valid image_url clears that person’s cached portraits.

Backup your database by copying /data folder.

Logging and privacy

Persistent log: <storage>/prks-errors.log. Default persistent threshold is ERROR. Rotation is daily at midnight. Retention is 7 days.

PRKS logs describe operations and failures, not the contents of the research library. Increasing PRKS_LOG_LEVEL or PRKS_LOG_FILE_LEVEL (including DEBUG) changes volume, not privacy policy.

Logs may include:

  • event names
  • request IDs
  • safe endpoint templates (/api/search, /api/pdfs/:pdf, /api/works/:id)
  • HTTP status
  • internal opaque IDs (work_id, processing_file_id)
  • counts, page numbers, byte ranges, file sizes
  • exception class names
  • repository-relative traceback locations

PRKS deliberately does not log:

  • research titles, notes, abstracts, annotations, or selected PDF text
  • person, tag, folder, or publisher names
  • search terms
  • PDF filenames or absolute filesystem paths
  • source, portrait, or image URLs
  • request bodies, query strings, or headers (Host, Origin, User-Agent, …)
  • client/LAN IP addresses
  • browser messages, stacks, routes, or hash state
  • raw exception messages
  • qpdf stderr

Docker captures process stderr. Console output follows the same privacy rules as the persistent file.

There is no remote telemetry. POST /api/client-errors is same-application metadata for correlating browser failures with server request IDs.

Development and tests

python run_tests.py

This discovers tests under tests/. run_tests.py always forces PRKS_TESTING=1 and PRKS_STORAGE to the repo’s data_testing/ directory and clears PRKS_FOR_PROCESSING_DIR and PRKS_LOG_FILE. That is stricter than python prks_app.py --testing, which may honor an explicit safe PRKS_STORAGE. Neither path uses ./data or container /data.

Project layout

PathRole
prks_app.pyOnly process entry: parses --testing, --port, --host, starts the server.
backend/server.pyHTTP handler: static frontend, REST-style /api/... routes.
backend/storage/config.pyFrozen storage snapshot and env parser.
backend/storage/paths.pyPath derivation and testing-mode containment.
backend/db_manager.pySQLite access and business logic.
backend/db_schema.sqlSchema and FTS triggers.
frontend/Static SPA (HTML, CSS, JS), PWA assets.
data/Default production database and files (gitignored as appropriate).
data_testing/Test fixtures and isolated DB/PDFs for automated tests.
tests/unittest modules.

Security note

PRKS is a single-user app with no built-in authentication. Direct runs bind 127.0.0.1 by default. Docker Compose publishes the host port on 127.0.0.1 by default. Reaching it from another machine requires an explicit --host or PRKS_PUBLISH_HOST override. Do that only on a trusted network.

Local browser use through http://127.0.0.1:8080 or http://localhost:8080 works without extra Host configuration. LAN access by IP literal (after PRKS_PUBLISH_HOST=0.0.0.0) also needs no PRKS_TRUSTED_HOSTS setting.

Custom LAN DNS names must be listed exactly:

PRKS_PUBLISH_HOST=0.0.0.0 \
PRKS_TRUSTED_HOSTS=prks.home.arpa \
docker compose up -d

Malformed PRKS_TRUSTED_HOSTS entries refuse to start the server. This variable is for extra DNS hostnames on direct HTTP/LAN access, not reverse-proxy or HTTPS termination.

The HTTP adapter validates Host on every request, rejects cross-origin state-changing /api/ requests when Origin is supplied (Origin: null included), and requires application/json for JSON POST/PATCH bodies. Missing Origin remains allowed for local scripts and non-browser clients. PRKS does not send CORS headers and does not allow cross-origin API access.

These controls reduce accidental/cross-origin access and DNS-rebinding risk. They are not authentication. Public Internet exposure is still unsafe.

Research notes (works.text_content) are stored as raw Markdown. Preview HTML is produced by EasyMDE/Marked and then sanitized with a pinned local DOMPurify allowlist (frontend/vendor/dompurify, frontend/js/markdown-sanitize.js). Arbitrary or active HTML is not a supported contract: unsafe tags, attributes, and URL schemes are stripped from the preview only. Sanitization never rewrites saved Markdown.

Frontend libraries (Inter, EasyMDE, CodeMirror, Lucide, DOMPurify, the PDF viewer) are local files under frontend/vendor/. Node is not a runtime dependency. Docker does not run npm. To rebuild the PDF viewer after changing tools/pdf-viewer/:

cd tools/pdf-viewer
npm ci
npm run build

That writes frontend/vendor/prks-pdf-viewer/ (EmbedPDF 2.15.0 + React 18.3.1, bundled). React is not part of the PRKS UI; it exists only inside that file. The PDF fixture is served by the same test-only server as the sanitizer fixture: open the printed tests/browser/pdf_viewer.html URL. It must report PASS with no jsDelivr / Google Fonts / unpkg requests.

The sanitizer-boundary browser fixture is not served by the app. From the repo root:

python tests/browser/serve.py

Open the printed 127.0.0.1 URL (and the ?dompurify=absent / ?dompurify=unsupported variants). The fixture must report PASS. Do not use production data/ or a live PRKS_STORAGE tree for this check.

About

Personal Research Knowledge System

Resources

Stars

0 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PRKS — Personal Research Knowledge System

PRKS is a self-hosted web application for organizing research materials: PDFs, Markdown notes, and online video references. It stores everything in a SQLite database and on-disk files on your machine—no separate database server. The UI supports folders, tags, reading progress, people and bibliographic metadata, PDF annotations, and playlists for videos.

Folder of public-domain books

Origin of Species open in the PDF reader

People in the library

Requirements

  • Python 3.12+
  • PyMuPDF 1.24.10

The HTTP server and SQLite access use the Python standard library.

Quick start (local)

From the repository root:

pip install -r requirements.txt
python prks_app.py

The process listens on 127.0.0.1:8080 only. Open http://127.0.0.1:8080 in a browser. No extra firewall or network setup is required for this case.

Optional port (still loopback):

python prks_app.py --port 9000

To listen on every local interface (LAN or VPN), pass an explicit host:

python prks_app.py --host 0.0.0.0

--host localhost also works and binds that name. The default is the literal address 127.0.0.1, not localhost.

Testing mode (Creates seperate testing database)

python prks_app.py --testing

This sets PRKS_TESTING=1 and uses port 8070 by default (unless you pass --port). With PRKS_STORAGE unset it defaults to data_testing/ so repo data/ is untouched. You may set PRKS_STORAGE to an explicit safe testing root. Testing mode refuses /data and the repository data/ directory (and descendants), including via symlinks. prks_app.py is the only process entry.

Docker

Build:

Use ./docker-build.sh, which builds prks:latest and prunes dangling images (from previous builds).

And run with Compose (from the repo root):

docker compose up -d

The container process binds 0.0.0.0:8080 so Docker port forwarding can reach it. Compose then publishes that port on the host loopback only (127.0.0.1:8080:8080). Container 0.0.0.0 is not the same as exposing PRKS on every host interface.

Open http://127.0.0.1:8080 on the machine that runs Compose. This also sets PRKS_STORAGE=/data, mounts ./data on the host to /data in the container, and runs the process as ${UID:-1000}:${GID:-1000} so files on the bind mount match your user. The entrypoint creates /data/pdfs if needed and runs python /app/prks_app.py --host 0.0.0.0.

To publish the host port on every interface (LAN access):

PRKS_PUBLISH_HOST=0.0.0.0 docker compose up -d

PRKS has no application-level authentication. Use that override only on a network you already trust, or behind an access layer you control.

Configuration and data layout

VariablePurpose
PRKS_STORAGEIf set, root directory for persistent data. Database: $PRKS_STORAGE/prks_data.db. PDFs: $PRKS_STORAGE/pdfs/. Thumbnails: $PRKS_STORAGE/thumbs/.
PRKS_TESTINGWhen truthy (1, true, yes), uses testing paths and stricter checks (see testing mode above).
PRKS_THUMB_LOSSLESSWhen truthy, PDF card thumbnails use lossless WebP/PNG cache encoding (debugging). Default is card-optimized lossy WebP; cache filenames use rev _v2.
PRKS_LOG_LEVELStderr log level. Default INFO. Changes volume, not what kinds of data may be logged.
PRKS_LOG_FILE_LEVELPersistent file log level. Default ERROR.
PRKS_LOG_RETENTION_DAYSRotated persistent log copies to keep. Default 7.
PRKS_LOG_FILEOverride path for the rotating error log. Default $PRKS_STORAGE/prks-errors.log.

If PRKS_STORAGE is unset, non-testing runs use the project’s data/ directory: data/prks_data.db, data/pdfs/, data/thumbs/, and person portrait cache data/people/ (lossy WebP, max 512px edge, keyed by person id + image_url hash).

Person profile images (GET /api/persons/{id}/profile-image) are optional. image_url must be a direct public HTTP/HTTPS URL (HTTPS preferred) that itself returns HTTP 200. PRKS does not follow redirects, and private/local/link-local targets are refused. Only static JPEG/PNG/WebP/GIF rasters are accepted. The download is size- and time-bounded; the image is decoded and transcoded (max 512px edge, usually WebP) before anything is cached. Original remote bytes are not kept. Local portrait upload is not part of this feature. Updating a valid image_url clears that person’s cached portraits.

Backup your database by copying /data folder.

Logging and privacy

Persistent log: <storage>/prks-errors.log. Default persistent threshold is ERROR. Rotation is daily at midnight. Retention is 7 days.

PRKS logs describe operations and failures, not the contents of the research library. Increasing PRKS_LOG_LEVEL or PRKS_LOG_FILE_LEVEL (including DEBUG) changes volume, not privacy policy.

Logs may include:

  • event names
  • request IDs
  • safe endpoint templates (/api/search, /api/pdfs/:pdf, /api/works/:id)
  • HTTP status
  • internal opaque IDs (work_id, processing_file_id)
  • counts, page numbers, byte ranges, file sizes
  • exception class names
  • repository-relative traceback locations

PRKS deliberately does not log:

  • research titles, notes, abstracts, annotations, or selected PDF text
  • person, tag, folder, or publisher names
  • search terms
  • PDF filenames or absolute filesystem paths
  • source, portrait, or image URLs
  • request bodies, query strings, or headers (Host, Origin, User-Agent, …)
  • client/LAN IP addresses
  • browser messages, stacks, routes, or hash state
  • raw exception messages
  • qpdf stderr

Docker captures process stderr. Console output follows the same privacy rules as the persistent file.

There is no remote telemetry. POST /api/client-errors is same-application metadata for correlating browser failures with server request IDs.

Development and tests

python run_tests.py

This discovers tests under tests/. run_tests.py always forces PRKS_TESTING=1 and PRKS_STORAGE to the repo’s data_testing/ directory and clears PRKS_FOR_PROCESSING_DIR and PRKS_LOG_FILE. That is stricter than python prks_app.py --testing, which may honor an explicit safe PRKS_STORAGE. Neither path uses ./data or container /data.

Project layout

PathRole
prks_app.pyOnly process entry: parses --testing, --port, --host, starts the server.
backend/server.pyHTTP handler: static frontend, REST-style /api/... routes.
backend/storage/config.pyFrozen storage snapshot and env parser.
backend/storage/paths.pyPath derivation and testing-mode containment.
backend/db_manager.pySQLite access and business logic.
backend/db_schema.sqlSchema and FTS triggers.
frontend/Static SPA (HTML, CSS, JS), PWA assets.
data/Default production database and files (gitignored as appropriate).
data_testing/Test fixtures and isolated DB/PDFs for automated tests.
tests/unittest modules.

Security note

PRKS is a single-user app with no built-in authentication. Direct runs bind 127.0.0.1 by default. Docker Compose publishes the host port on 127.0.0.1 by default. Reaching it from another machine requires an explicit --host or PRKS_PUBLISH_HOST override. Do that only on a trusted network.

Local browser use through http://127.0.0.1:8080 or http://localhost:8080 works without extra Host configuration. LAN access by IP literal (after PRKS_PUBLISH_HOST=0.0.0.0) also needs no PRKS_TRUSTED_HOSTS setting.

Custom LAN DNS names must be listed exactly:

PRKS_PUBLISH_HOST=0.0.0.0 \
PRKS_TRUSTED_HOSTS=prks.home.arpa \
docker compose up -d

Malformed PRKS_TRUSTED_HOSTS entries refuse to start the server. This variable is for extra DNS hostnames on direct HTTP/LAN access, not reverse-proxy or HTTPS termination.

The HTTP adapter validates Host on every request, rejects cross-origin state-changing /api/ requests when Origin is supplied (Origin: null included), and requires application/json for JSON POST/PATCH bodies. Missing Origin remains allowed for local scripts and non-browser clients. PRKS does not send CORS headers and does not allow cross-origin API access.

These controls reduce accidental/cross-origin access and DNS-rebinding risk. They are not authentication. Public Internet exposure is still unsafe.

Research notes (works.text_content) are stored as raw Markdown. Preview HTML is produced by EasyMDE/Marked and then sanitized with a pinned local DOMPurify allowlist (frontend/vendor/dompurify, frontend/js/markdown-sanitize.js). Arbitrary or active HTML is not a supported contract: unsafe tags, attributes, and URL schemes are stripped from the preview only. Sanitization never rewrites saved Markdown.

Frontend libraries (Inter, EasyMDE, CodeMirror, Lucide, DOMPurify, the PDF viewer) are local files under frontend/vendor/. Node is not a runtime dependency. Docker does not run npm. To rebuild the PDF viewer after changing tools/pdf-viewer/:

cd tools/pdf-viewer
npm ci
npm run build

That writes frontend/vendor/prks-pdf-viewer/ (EmbedPDF 2.15.0 + React 18.3.1, bundled). React is not part of the PRKS UI; it exists only inside that file. The PDF fixture is served by the same test-only server as the sanitizer fixture: open the printed tests/browser/pdf_viewer.html URL. It must report PASS with no jsDelivr / Google Fonts / unpkg requests.

The sanitizer-boundary browser fixture is not served by the app. From the repo root:

python tests/browser/serve.py

Open the printed 127.0.0.1 URL (and the ?dompurify=absent / ?dompurify=unsupported variants). The fixture must report PASS. Do not use production data/ or a live PRKS_STORAGE tree for this check.

About

Personal Research Knowledge System

Resources

Stars

0 stars

Watchers

1 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

PRKS — Personal Research Knowledge System

PRKS is a self-hosted web application for organizing research materials: PDFs, Markdown notes, and online video references. It stores everything in a SQLite database and on-disk files on your machine—no separate database server. The UI supports folders, tags, reading progress, people and bibliographic metadata, PDF annotations, and playlists for videos.

Folder of public-domain books

Origin of Species open in the PDF reader

People in the library

Requirements

  • Python 3.12+
  • PyMuPDF 1.24.10

The HTTP server and SQLite access use the Python standard library.

Quick start (local)

From the repository root:

pip install -r requirements.txt
python prks_app.py

The process listens on 127.0.0.1:8080 only. Open http://127.0.0.1:8080 in a browser. No extra firewall or network setup is required for this case.

Optional port (still loopback):

python prks_app.py --port 9000

To listen on every local interface (LAN or VPN), pass an explicit host:

python prks_app.py --host 0.0.0.0

--host localhost also works and binds that name. The default is the literal address 127.0.0.1, not localhost.

Testing mode (Creates seperate testing database)

python prks_app.py --testing

This sets PRKS_TESTING=1 and uses port 8070 by default (unless you pass --port). With PRKS_STORAGE unset it defaults to data_testing/ so repo data/ is untouched. You may set PRKS_STORAGE to an explicit safe testing root. Testing mode refuses /data and the repository data/ directory (and descendants), including via symlinks. prks_app.py is the only process entry.

Docker

Build:

Use ./docker-build.sh, which builds prks:latest and prunes dangling images (from previous builds).

And run with Compose (from the repo root):

docker compose up -d

The container process binds 0.0.0.0:8080 so Docker port forwarding can reach it. Compose then publishes that port on the host loopback only (127.0.0.1:8080:8080). Container 0.0.0.0 is not the same as exposing PRKS on every host interface.

Open http://127.0.0.1:8080 on the machine that runs Compose. This also sets PRKS_STORAGE=/data, mounts ./data on the host to /data in the container, and runs the process as ${UID:-1000}:${GID:-1000} so files on the bind mount match your user. The entrypoint creates /data/pdfs if needed and runs python /app/prks_app.py --host 0.0.0.0.

To publish the host port on every interface (LAN access):

PRKS_PUBLISH_HOST=0.0.0.0 docker compose up -d

PRKS has no application-level authentication. Use that override only on a network you already trust, or behind an access layer you control.

Configuration and data layout

VariablePurpose
PRKS_STORAGEIf set, root directory for persistent data. Database: $PRKS_STORAGE/prks_data.db. PDFs: $PRKS_STORAGE/pdfs/. Thumbnails: $PRKS_STORAGE/thumbs/.
PRKS_TESTINGWhen truthy (1, true, yes), uses testing paths and stricter checks (see testing mode above).
PRKS_THUMB_LOSSLESSWhen truthy, PDF card thumbnails use lossless WebP/PNG cache encoding (debugging). Default is card-optimized lossy WebP; cache filenames use rev _v2.
PRKS_LOG_LEVELStderr log level. Default INFO. Changes volume, not what kinds of data may be logged.
PRKS_LOG_FILE_LEVELPersistent file log level. Default ERROR.
PRKS_LOG_RETENTION_DAYSRotated persistent log copies to keep. Default 7.
PRKS_LOG_FILEOverride path for the rotating error log. Default $PRKS_STORAGE/prks-errors.log.

If PRKS_STORAGE is unset, non-testing runs use the project’s data/ directory: data/prks_data.db, data/pdfs/, data/thumbs/, and person portrait cache data/people/ (lossy WebP, max 512px edge, keyed by person id + image_url hash).

Person profile images (GET /api/persons/{id}/profile-image) are optional. image_url must be a direct public HTTP/HTTPS URL (HTTPS preferred) that itself returns HTTP 200. PRKS does not follow redirects, and private/local/link-local targets are refused. Only static JPEG/PNG/WebP/GIF rasters are accepted. The download is size- and time-bounded; the image is decoded and transcoded (max 512px edge, usually WebP) before anything is cached. Original remote bytes are not kept. Local portrait upload is not part of this feature. Updating a valid image_url clears that person’s cached portraits.

Backup your database by copying /data folder.

Logging and privacy

Persistent log: <storage>/prks-errors.log. Default persistent threshold is ERROR. Rotation is daily at midnight. Retention is 7 days.

PRKS logs describe operations and failures, not the contents of the research library. Increasing PRKS_LOG_LEVEL or PRKS_LOG_FILE_LEVEL (including DEBUG) changes volume, not privacy policy.

Logs may include:

  • event names
  • request IDs
  • safe endpoint templates (/api/search, /api/pdfs/:pdf, /api/works/:id)
  • HTTP status
  • internal opaque IDs (work_id, processing_file_id)
  • counts, page numbers, byte ranges, file sizes
  • exception class names
  • repository-relative traceback locations

PRKS deliberately does not log:

  • research titles, notes, abstracts, annotations, or selected PDF text
  • person, tag, folder, or publisher names
  • search terms
  • PDF filenames or absolute filesystem paths
  • source, portrait, or image URLs
  • request bodies, query strings, or headers (Host, Origin, User-Agent, …)
  • client/LAN IP addresses
  • browser messages, stacks, routes, or hash state
  • raw exception messages
  • qpdf stderr

Docker captures process stderr. Console output follows the same privacy rules as the persistent file.

There is no remote telemetry. POST /api/client-errors is same-application metadata for correlating browser failures with server request IDs.

Development and tests

python run_tests.py

This discovers tests under tests/. run_tests.py always forces PRKS_TESTING=1 and PRKS_STORAGE to the repo’s data_testing/ directory and clears PRKS_FOR_PROCESSING_DIR and PRKS_LOG_FILE. That is stricter than python prks_app.py --testing, which may honor an explicit safe PRKS_STORAGE. Neither path uses ./data or container /data.

Project layout

PathRole
prks_app.pyOnly process entry: parses --testing, --port, --host, starts the server.
backend/server.pyHTTP handler: static frontend, REST-style /api/... routes.
backend/storage/config.pyFrozen storage snapshot and env parser.
backend/storage/paths.pyPath derivation and testing-mode containment.
backend/db_manager.pySQLite access and business logic.
backend/db_schema.sqlSchema and FTS triggers.
frontend/Static SPA (HTML, CSS, JS), PWA assets.
data/Default production database and files (gitignored as appropriate).
data_testing/Test fixtures and isolated DB/PDFs for automated tests.
tests/unittest modules.

Security note

PRKS is a single-user app with no built-in authentication. Direct runs bind 127.0.0.1 by default. Docker Compose publishes the host port on 127.0.0.1 by default. Reaching it from another machine requires an explicit --host or PRKS_PUBLISH_HOST override. Do that only on a trusted network.

Local browser use through http://127.0.0.1:8080 or http://localhost:8080 works without extra Host configuration. LAN access by IP literal (after PRKS_PUBLISH_HOST=0.0.0.0) also needs no PRKS_TRUSTED_HOSTS setting.

Custom LAN DNS names must be listed exactly:

PRKS_PUBLISH_HOST=0.0.0.0 \
PRKS_TRUSTED_HOSTS=prks.home.arpa \
docker compose up -d

Malformed PRKS_TRUSTED_HOSTS entries refuse to start the server. This variable is for extra DNS hostnames on direct HTTP/LAN access, not reverse-proxy or HTTPS termination.

The HTTP adapter validates Host on every request, rejects cross-origin state-changing /api/ requests when Origin is supplied (Origin: null included), and requires application/json for JSON POST/PATCH bodies. Missing Origin remains allowed for local scripts and non-browser clients. PRKS does not send CORS headers and does not allow cross-origin API access.

These controls reduce accidental/cross-origin access and DNS-rebinding risk. They are not authentication. Public Internet exposure is still unsafe.

Research notes (works.text_content) are stored as raw Markdown. Preview HTML is produced by EasyMDE/Marked and then sanitized with a pinned local DOMPurify allowlist (frontend/vendor/dompurify, frontend/js/markdown-sanitize.js). Arbitrary or active HTML is not a supported contract: unsafe tags, attributes, and URL schemes are stripped from the preview only. Sanitization never rewrites saved Markdown.

Frontend libraries (Inter, EasyMDE, CodeMirror, Lucide, DOMPurify, the PDF viewer) are local files under frontend/vendor/. Node is not a runtime dependency. Docker does not run npm. To rebuild the PDF viewer after changing tools/pdf-viewer/:

cd tools/pdf-viewer
npm ci
npm run build

That writes frontend/vendor/prks-pdf-viewer/ (EmbedPDF 2.15.0 + React 18.3.1, bundled). React is not part of the PRKS UI; it exists only inside that file. The PDF fixture is served by the same test-only server as the sanitizer fixture: open the printed tests/browser/pdf_viewer.html URL. It must report PASS with no jsDelivr / Google Fonts / unpkg requests.

The sanitizer-boundary browser fixture is not served by the app. From the repo root:

python tests/browser/serve.py

Open the printed 127.0.0.1 URL (and the ?dompurify=absent / ?dompurify=unsupported variants). The fixture must report PASS. Do not use production data/ or a live PRKS_STORAGE tree for this check.

About

Personal Research Knowledge System

Resources

Stars

0 stars

Watchers

1 watching

Forks

Contributors

Languages