- Collab buckets sync attachments alongside cards (images/PDFs/audio). New /api/collab-attachments endpoint on relay, encrypted at rest, bucket-scoped for members. - CalDAV multi-day events via **Bis:** body field. Calendar renders event on every day in range with day counter (1/6, 2/6, ...). - Calendar preserves month across close/reopen (only jumps to today on first open). - Calendar title: centered nav group with [MM-DD] Mon Y [KW-KW] — dimmed brackets, fixed width so nothing wiggles. - Editor: scratchpad uses custom confirm, autobackup-draft dialog replaced with custom styled dialog. - Scanlines theme feature removed (performance issue). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
hyprcrd
A self-hosted card/note manager with markdown, tags, image galleries, and Obsidian-compatible export.
Requirements
- PHP 8.3+
- Apache with
mod_rewrite - PHP extensions:
pdo_sqlite,zip
No external dependencies. All assets are bundled locally.
Quick Start (Docker)
git clone https://git.uesome.de/tanztee/hyprcrd.git
cd hyprcrd
docker build -t hyprcrd .
docker run -d -p 8080:80 -v hyprcrd-data:/var/www/html/data hyprcrd
Open http://localhost:8080 and set your password on first visit.
The -v hyprcrd-data:/var/www/html/data flag persists the database across container restarts.
Manual Installation
1. Clone
git clone https://git.uesome.de/tanztee/hyprcrd.git
cd hyprcrd
2. Set document root
Point your web server's document root to the public/ directory.
Apache vhost example:
<VirtualHost *:80>
ServerName hyprcrd.local
DocumentRoot /path/to/hyprcrd/public
<Directory /path/to/hyprcrd/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
Make sure mod_rewrite is enabled:
a2enmod rewrite
systemctl restart apache2
3. Set permissions
mkdir -p data public/uploads
chmod 755 data
chmod 777 public/uploads
The web server user (e.g. www-data) needs write access to both directories.
4. PHP settings (optional)
For larger image uploads, adjust in php.ini:
upload_max_filesize = 20M
post_max_size = 22M
5. Open and set password
Navigate to your URL. On first visit, you'll be prompted to set a password (min. 4 characters). That's it.
Directory Structure
hyprcrd/
├── config.php # App constants (auto-configured)
├── Dockerfile
├── docker-entrypoint.sh
├── dist.sh # Build distribution ZIP
├── src/
│ ├── bootstrap.php # Initialization + plugin discovery
│ ├── Database.php # SQLite schema & migrations
│ ├── PluginManager.php # Plugin scanner & asset/slot renderer
│ ├── CalDavClient.php # CalDAV HTTP client + iCal parser
│ └── CalDavSync.php # Bidirectional CalDAV sync engine
├── public/ # Document root
│ ├── index.php # Entry point & API
│ ├── .htaccess # URL rewriting
│ ├── css/
│ │ ├── style.css # App styles
│ │ ├── tailwind.out.css # Generated Tailwind utilities
│ │ ├── fonts.css # Local @font-face rules
│ │ └── vendor/ # Phosphor Icons CSS + fonts
│ ├── fonts/ # JetBrains Mono & Silkscreen
│ ├── js/
│ │ ├── hyprcrd.js # Plugin API (window.hyprcrd, sync-loaded)
│ │ ├── app.js # Orchestrator
│ │ ├── modules/ # Core feature modules
│ │ └── vendor/ # marked.js, DOMPurify
│ ├── plugins/ # Optional, drop-in plugin folders
│ │ ├── canvas/ # ASCII art canvas editor
│ │ ├── chat/ # Encrypted relay chat
│ │ ├── collab-buckets/ # Collaborative bucket sync
│ │ ├── email/ # Email overlay
│ │ ├── gitea/ # Gitea issue integration
│ │ ├── logseq-import/ # Logseq markdown importer
│ │ ├── slideshow/ # Presentation mode
│ │ └── tests/ # UI + state test runner
│ └── uploads/ # User media (writable)
├── electron/ # Electron desktop app wrapper
│ ├── main.js # Spawns PHP, creates BrowserWindow
│ ├── router.php # PHP built-in server router
│ ├── fetch-php.sh # Downloads static PHP binaries
│ └── php-bin/ # Platform-specific PHP binaries
├── relay/ # Relay service (multi-tenant messaging + collab)
│ ├── index.php # Entry point + API dispatch
│ ├── inc/ # Backend modules (bootstrap, crypto, auth, routes)
│ ├── ui/ # Frontend (layout.php, style.css, app.js)
│ ├── .htaccess # URL rewriting + data protection
│ ├── Dockerfile
│ └── dist.sh # Build relay distribution ZIP
└── data/
└── hyprcrd.db # SQLite database (auto-created)
Features
- Cards with markdown body, color, tags, and position
- Buckets for organizing cards into groups
- Tags written as
#tagnameor hierarchical#parent/childin card body, extracted automatically - Markdown with
[[Card Title]]wiki-links,```galleryimage galleries,```treelist views,```asciiart canvas (via canvas plugin) - Search combining text and
#tagfilters (e.g.#project meeting notes) - Assets gallery (images, audio, PDFs) with orphan detection and fullscreen viewer
- Split view showing tag-related and content-related cards
- Import/Export as JSON+media ZIP or Obsidian/Logseq-compatible markdown vault
- Drag & drop card reordering and grouping
- Calendar with month view, journal entries, appointments, and day popup
- CalDAV sync with ownCloud/Nextcloud (bidirectional,
#termintagged cards) - Inline calculator (
?3+5→8,34+56?→34+56=90) - Snippets (
?dat,?time,?now,?todo,?table,?code, ...) - Due dates with overdue notifications and pulsing cards
- Relay for mobile note capture and card sharing between instances
- Notification banner for upcoming events, birthdays, overdue tasks
- Board split view with side-by-side preview
- Superview compact multi-column list with hover cards
- Super-buckets — nest buckets one level deep with a dropdown for children
- Card groups — drag-to-group, join multiple cards, dissolve
- Context menu — color, tags, copy text, mark done, join cards, make ticket (gitea)
- Find & replace — in-card (Ctrl+H) and across all cards (Ctrl+Shift+H via search)
- Arrow key navigation on the board with Enter to open
- PDF upload in the editor toolbar
- Keyboard shortcuts:
Ctrl+Ksearch,F1help,?shortcut reference, arrow keys, 1–9 bucket switch - Electron desktop app — standalone AppImage/DMG/NSIS with bundled PHP, no server needed
Plugins
hyprcrd has a drop-in plugin system. A plugin is just a folder under public/plugins/ with a plugin.json manifest. The core scans the directory at startup, loads each plugin's CSS/JS/HTML, and the plugin self-registers against window.hyprcrd. Plugins are entirely optional — removing a plugin folder removes the feature, with no code changes anywhere else.
Bundled plugins
canvas/— ASCII art canvas editor. Adds a toolbar button +```asciifenced block. Draw with brush, line, rectangle, ellipse, fill, and text tools using a monospace character grid.chat/— Encrypted chat between relay tenants. Footer flyout with delivery + read receipts.collab-buckets/— Collaborative buckets shared between relay tenants with exclusive card locking and E2E encrypted sync.email/— Email overlay (toggled withMkey).gitea/— Gitea integration: create issues from cards, link to repos.slideshow/— Slideshow presentation mode for cards withslide-imageblocks.logseq-import/— Import Logseq markdown journals and pages.tests/— UI + state test runner (40+ test files). Loaded only whenCtrl+Shift+Alt+Tis pressed, so production users pay no cost for it.
Plugin layout
public/plugins/myplugin/
├── plugin.json # Manifest (required)
├── myplugin.js # Entry point (loaded as ES module)
├── myplugin.css # Optional styles
├── myplugin.html # Optional HTML fragment (overlays, modals, etc.)
└── backend.php # Optional PHP hooks
plugin.json:
{
"name": "myplugin",
"version": "1.0.0",
"description": "What this plugin does",
"frontend": {
"js": ["myplugin.js"],
"css": ["myplugin.css"],
"html": ["myplugin.html"]
},
"backend": "backend.php"
}
Plugin API (window.hyprcrd)
Loaded synchronously before any module, so plugins can register handlers immediately:
| Namespace | Methods | Purpose |
|---|---|---|
hyprcrd.editor |
registerCommand(name, fn), getCommand(name), api.{body, insertMd, updatePreview} |
Add toolbar commands. The data-md="<name>" button dispatches to the registered handler. api is set after the editor initializes. |
hyprcrd.markdown |
registerFencedBlock(lang, fn), registerPreProcessor(fn), registerPostProcessor(fn) |
Add ```<lang> block renderers (returns HTML, passes through DOMPurify). Pre/post processors run on the source/HTML around marked.parse(). |
hyprcrd.hooks |
on(name, fn), off(name, fn), emit(name, data) |
Generic event bus. Use for cross-plugin or core→plugin signaling. |
hyprcrd.util |
$(idOrSel), $$(sel), esc(s) |
Lightweight DOM/escape helpers so plugins don't need to import core modules. |
hyprcrd.registerPlugin(meta) |
— | Register the plugin's metadata for introspection. |
Minimal plugin example
public/plugins/hello/plugin.json:
{
"name": "hello",
"version": "1.0.0",
"description": "Inserts a greeting",
"frontend": { "js": ["hello.js"] }
}
public/plugins/hello/hello.js:
const h = window.hyprcrd;
h.editor.registerCommand('hello', () => {
h.editor.api.insertMd('Hello, world! ', '', '');
});
// Inject a toolbar button
const btn = document.createElement('button');
btn.dataset.md = 'hello';
btn.className = 'toolbar-btn';
btn.title = 'Hello';
btn.textContent = '👋';
document.getElementById('md-toolbar').appendChild(btn);
h.registerPlugin({ name: 'hello', version: '1.0.0' });
That's the entire plugin. Drop the folder in, reload, the button appears.
Backend hooks (PHP)
If a plugin needs server-side code, set "backend": "backend.php" in the manifest. The file is require_once'd during bootstrap. Backend plugins can use PluginManager::instance()->addToSlot('slot-name', '<html>') to inject HTML into named slots in index.php (e.g. md-toolbar).
Removing a plugin
rm -rf public/plugins/canvas
That's it. The toolbar button, overlay, CSS, and markdown handler all disappear together. The core has no awareness of any specific plugin.
Data & Backup
All data lives in two places:
data/hyprcrd.db— SQLite database (cards, buckets, settings)public/uploads/— uploaded images and audio
Back up both to preserve everything. The database auto-migrates on startup.
Export Formats
- JSON export (
exportbutton) — full backup with media, re-importable - Markdown vault (
vaultbutton) — Obsidian/Logseq-compatible structure:Bucket Name/ Card Title.md # YAML frontmatter + markdown body attachments/ image.jpg
CalDAV Sync (ownCloud / Nextcloud)
hyprcrd can sync appointment cards with any CalDAV server (tested with ownCloud 10.15.3.0).
Setup
- Open the calendar (button in header)
- Click the gear icon in the calendar header
- Enter your CalDAV URL, username, and app password:
URL: https://cloud.example.com/remote.php/dav/calendars/your-user/personal/ Username: your-user Password: your-app-password - Click test to verify the connection
- Click save
How it works
- Cards tagged with
#terminare synced as calendar events - The appointment card template maps to iCalendar fields:
Title: 2026-04-15 — Team Meeting **Zeit:** 14:30-15:30 **Ort:** Room 42 **Teilnehmer:** Alice, Bob --- **Notizen:** Agenda items here #termin - Sync is bidirectional: local changes push to the server, remote changes pull to hyprcrd
- Conflict resolution: last-modified timestamp wins
- Deleted cards: tracked via tombstones, deleted on the server on next sync
- Auto-sync: triggers when opening the calendar (max once per minute)
- Manual sync: click the cloud icon in the calendar header
Security
- Credentials are encrypted at rest (AES-256-CBC) in the SQLite database
- All sync operations run server-side (PHP/cURL), no client-side credential exposure
- Supports HTTPS with certificate verification
Calendar
The built-in calendar shows cards on their dates:
- Journal cards (title =
2026-04-08) appear on that date - Appointment cards (title =
2026-04-08 — Subject) appear on their date - Other cards fall back to their creation date
- Click a day to see all cards in a popup
- Double-click or use + journal / + termin to create new entries
- Appointment template includes Zeit, Ort, Teilnehmer, Notizen fields
- Navigate months with arrow buttons, today jumps to current date
Vault Key (Multi-Instance Encryption)
When running multiple hyprcrd instances that share CalDAV credentials, you need a shared encryption key.
Setup
- Go to Settings > Account > Vault Key
- Enter the same key (min 8 characters) on all instances
- Click save
How it works
- The vault key encrypts/decrypts CalDAV credentials stored in the database
- Key derivation:
SHA-256(vault_key)→ deterministic, same on all instances - Without a vault key, encryption falls back to the login password hash (single-instance only)
- Changing the vault key automatically re-encrypts CalDAV credentials
- Changing the login password does NOT affect encryption when a vault key is set
Important
- The vault key is stored in plaintext in each instance's database — it is NOT synced
- You must enter it manually on each connected instance
Relay
A self-hosted multi-tenant message relay for hyprcrd. Multiple users share one relay instance — each one has their own login, their own client encryption secret, and their own inbox. Cards (and in the future: encrypted chat messages) can be sent between tenants on the same relay.
Full documentation: relay/README.md
Architecture
[hyprcrd alice] ──push──┐ ┌──poll── [hyprcrd alice]
├──► [Relay] ◄────────────┤
[hyprcrd bob] ──push──┘ tenants: alice, bob └──poll── [hyprcrd bob]
sender encrypts → relay decrypts with sender's secret →
re-encrypts with shared inter-tenant key → stores →
on recipient poll: decrypts → re-encrypts with recipient's secret
Deploy
Webspace (any PHP host with mod_rewrite and pdo_sqlite):
cd relay
./dist.sh
# upload hyprcrd-relay.zip and extract into your webspace
Docker:
cd relay
docker build -t hyprcrd-relay .
docker run -d -p 8081:80 -v relay-data:/var/www/html/data hyprcrd-relay
The data/ directory is created on first request and contains the
SQLite DB plus the master encryption key (chmod 600, blocked from web
access via .htaccess).
Development (both together)
./dev.sh
Starts hyprcrd on http://localhost:8088 and the relay on http://localhost:8081 in the same Docker network. Source files are bind-mounted — changes are live without rebuilding. From inside the hyprcrd container, the relay is reachable at http://relay:80.
Setup workflow
- Open the relay URL from a desktop browser — the setup wizard is desktop-friendly
- Create the first admin account: username + password (UI login)
- shared secret (client encryption)
- As admin, click the users tab and create accounts for any additional users you want to give relay access to
- Each user enters their credentials in their own hyprcrd: Settings → Relay → relay URL + username + shared secret
- To send a card from one tenant to another, use the relay UI's compose form (recipient picker) — or push cards from hyprcrd
How it works (high level)
- Per-tenant inboxes: each user only sees their own messages
- Cross-tenant routing: send a card to another tenant on the same relay; the relay decrypts with the sender's secret, re-encrypts with a shared inter-tenant key for storage, then re-encrypts with the recipient's secret on delivery
- At-rest encryption: every sensitive value in the DB (tenant secrets, the inter-tenant key, message bodies) is encrypted. The master key lives outside the DB in a chmod-600 file
- 24-hour ephemeral: messages and attachments auto-delete after a day. Cards extracted in hyprcrd are the long-term store
- Encrypted chat: tenant-to-tenant chat with delivery + read receipts (via chat plugin). Per-pair E2E key exchange is on the roadmap
Relay Security
- Per-tenant bcrypt-hashed passwords + secrets
- Master key encryption for tenant secrets and the inter-tenant key at rest
- HttpOnly + SameSite=Strict session cookies
- Rate-limited login (5 attempts/minute per IP)
- AES-256-CBC for all message encryption
- Security headers (X-Frame-Options, HSTS, X-Content-Type-Options)
data/directory blocked from direct download via.htaccess- Request size limited to 256KB (15MB for attachments)
- Last-admin protection prevents accidental lockout
For the full security model (including what's NOT protected and the
roadmap to strict E2E for chat), see relay/README.md.
Migrating from the old single-tenant relay
When you upgrade an existing relay installation, the new code automatically
detects the legacy schema and drops the old messages and attachments
tables (the data is 24h-ephemeral anyway). The setup wizard appears on
your next visit. You'll need to:
- Run through the desktop setup wizard to create the first admin
- Recreate any other tenant accounts that need access
- On every connected hyprcrd, update Settings → Relay to add the new username field and the new shared secret
The old relay_key was bcrypt-hashed and cannot be recovered, so all
client secrets need to be reissued.
Electron Desktop App
hyprcrd can run as a standalone desktop app via Electron — no web server needed.
cd electron
./fetch-php.sh # download static PHP binary (~20MB)
npm install
npm start # dev mode
npm run dist # build AppImage / DMG / NSIS
The app bundles a static PHP binary per platform and stores user data (database + uploads) outside the app bundle:
- Linux:
~/.config/hyprcrd/data/ - macOS:
~/Library/Application Support/hyprcrd/data/ - Windows:
%APPDATA%/hyprcrd/data/
See electron/README.md for details.
Rebuilding Tailwind CSS
After changing Tailwind utility classes in HTML or JS files:
# Download standalone CLI (once)
curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 -o tailwindcss && chmod +x tailwindcss
# Build (includes plugins so plugin HTML/JS contributes utility classes)
./tailwindcss --input public/css/tailwind.css --output public/css/tailwind.out.css --content "public/index.php,public/js/**/*.js,public/plugins/**/*.{js,html}" --minify
Security
- Bcrypt password hashing
- CSRF protection on all mutations
- Rate-limited login (5 attempts/60s)
- Upload directory blocks PHP execution
- DOMPurify sanitizes all rendered markdown
- Security headers (X-Frame-Options, HSTS, etc.)
- AES-256-CBC encryption for CalDAV credentials and Relay messages
- Vault key system for multi-instance CalDAV encryption (independent of login password)
- Relay: separate password + shared secret, rate-limited, client-side encryption
License
MIT