Intelligent, offline-first desktop note app. Solid core. Extensible. Monetizable. Maintainable 3-5 years.
Goal: Build a note-taking app that competes with Obsidian/Inkdrop through superior architecture, not feature bloat.
What Dripnex IS:
- Offline-first desktop app (Electron)
- Markdown-based with smart features (backlinks, tags, graph)
- Single-user, local-first (sync optional later)
- Premium product with clear monetization
What Dripnex is NOT:
- A Notion clone (no blocks-first architecture)
- A collaboration tool (not yet)
- A mobile app (desktop first)
Thesis: Dripnex is a Markdown-first app that happens to be smart — not a smart system that happens to use Markdown.
This means:
- Markdown preservation > intelligence features
- Export fidelity > internal efficiency
- User trust > clever optimization
One-liner: "Your notes survive the app."
Competitive position:
- Not competing with Inkdrop (closed DB, sync-first)
- Competing with: Obsidian (but more coherent), VS Code + Markdown (but real product), "my folder of notes I don't want to lose"
| Principle | Rule |
|---|---|
| Core first | Core runs without Electron, React, or UI. Testable in pure Node. |
| Offline-first | 100% functional without internet. Sync is a feature, not a requirement. |
| Strict separation | Core ≠ UI ≠ Infra ≠ Packaging. React never decides domain logic. |
| Minimal deps | Every library needs written justification. No "just in case" abstractions. |
| Decision | Choice | Rationale |
|---|---|---|
| Monorepo | pnpm + turborepo | Isolate concerns, share core across apps |
| Electron tooling | electron-vite | Fast HMR, clear main/preload/renderer boundaries |
| Node version | 20 LTS | Stability |
| Decision | Choice | Rationale |
|---|---|---|
| Local DB | SQLite (main process) | Robust, file-based, portable, queryable |
| Access pattern | Typed repositories via IPC | No raw SQL in renderer |
| Migrations | Forward-only, versioned | Safe evolution |
| Decision | Choice | Rationale |
|---|---|---|
| Engine | CodeMirror 6 | Modular, extensible, performant |
| Relationship to core | Editor edits markdown directly | Markdown text is the canonical source |
| Decision | Choice | Rationale |
|---|---|---|
| Source of truth | Markdown text | Simple, portable, compatible with ecosystem |
| Domain model | AST + derived indices | Parsed from markdown, NOT a separate canonical structure |
| Derived data | title, tags, backlinks, headings | Computed on parse/save, stored as columns for queries |
| Storage | Full Markdown in SQLite + metadata columns | Fast queries + full content |
Clarification:
- Canonical = the markdown text itself
- Domain = AST parsed from markdown + derived indices (NOT a separate structure)
- Editor edits markdown; core parses/validates; indices are derived
- If parse fails, raw markdown is preserved (never lost)
| Invariant | Rule | Why |
|---|---|---|
| Raw text is sacred | User-typed markdown is NEVER auto-modified | Trust |
| AST is ephemeral | AST exists only for parsing, never persisted as authority | Simplicity |
| Parse errors don't block | If parse fails, save raw markdown anyway | Data safety |
| No normalization | Never reformat whitespace, headings, lists | Preserve intent |
| No serialization from AST | Never reconstruct markdown from parsed AST | Fidelity |
Golden rule: "If the user typed it, we keep it."
Consequences:
- No "prettify markdown" feature
- No auto-fix for broken links
- No whitespace normalization
- Export = exact copy of stored markdown
| Decision | Choice | Rationale |
|---|---|---|
| Async/server state | TanStack Query | Cache, sync, invalidation |
| UI-only state | Zustand | Minimal, no boilerplate |
| Avoid | Redux, global god-state | Unnecessary complexity |
| Decision | Choice | Rationale |
|---|---|---|
| Foundation | CSS Variables (tokens) | Themeable, simple |
| Primitives | Radix UI | Accessible, unstyled |
| Components | shadcn/ui (editable) | Copy-paste, full control |
| Utility | Tailwind (as engine only) | Fast iteration |
| Editor styles | Isolated scope | Never inherit from UI kit |
Principle: Start with 5 packages, not 9. Split when there's actual pain.
dripnex/
├── apps/
│ ├── desktop/ # Electron app
│ │ ├── src/main/ # Main process (SQLite, IPC handlers)
│ │ ├── src/preload/ # Secure bridge
│ │ └── src/renderer/ # React UI
│ ├── marketing-site/ # Landing, pricing
│ └── docs-site/ # Technical docs (VitePress)
├── packages/
│ ├── core/ # Domain + app layer + markdown parsing
│ ├── storage/ # SQLite adapter + migrations
│ ├── ipc-contract/ # Typed IPC channels (Zod schemas)
│ ├── licensing/ # License validation, trial
│ └── ui-kit/ # React components + design tokens
├── pnpm-workspace.yaml
└── turbo.json
Deferred to v0.2+ (split when needed):
packages/app— Extract from core when use-cases grow complexpackages/markdown— Extract when AST handling needs isolationpackages/product-config— Hardcode pricing in marketing + app for nowpackages/design-tokens— Keep inside ui-kit for now
Why simplify:
- Less package orchestration overhead
- Faster iteration for solo dev
- Split when there's actual pain, not theoretical purity
packages/core/
├── domain/
│ ├── note.ts # Note entity
│ ├── metadata.ts # Title, created, updated, tags
│ ├── link.ts # Backlink/forward link
│ └── invariants.ts # Business rules
├── operations/
│ ├── createNote.ts
│ ├── updateNote.ts
│ ├── deleteNote.ts
│ └── linkNotes.ts
├── queries/
│ ├── getNoteById.ts
│ ├── searchNotes.ts
│ └── getBacklinks.ts
├── contracts/
│ ├── NoteInput.ts # What enters the core
│ ├── NoteSnapshot.ts # What exits the core
│ └── CoreResult.ts # Operation results
├── validation/
│ └── schemas.ts # Zod schemas
└── index.ts # Public API
Core Runtime Contract:
- Core exposes Commands (mutations) and Queries (reads)
- UI calls use-cases, never internal functions
- All I/O goes through ports/adapters
Storage Contract:
interface NoteRepository {
get(id: NoteId): Promise<Note | null>;
save(note: Note): Promise<void>;
delete(id: NoteId): Promise<void>;
list(query: NoteQuery): Promise<Note[]>;
search(term: string): Promise<Note[]>;
}IPC Contract:
- All channels typed with Zod schemas in
packages/ipc-contract - No
executeSQLexposed to renderer - Batch APIs for performance (e.g.,
getNotesPagenotgetNoteByIdx100)
| Topic | Status | Notes |
|---|---|---|
| Plugin system | Deferred | Define boundary now, implement later |
| Monetization | ✅ Decided | Freemium + Subscription (see Section 18) |
| Auth & Backend | ✅ Decided | Inkdrop-style, Stripe, Stage 0/1/2 (see below) |
| Sync | Deferred (St. 2) | Schema ready, implement when demand exists |
| Full-text search | Deferred | SQLite FTS5 or separate index |
Model: Free Offline + Pro with Account (Inkdrop-style)
Stack (Stage 1):
- API: Fastify or Hono
- Auth: Propio (JWT + Argon2)
- DB: Postgres (Neon)
- Payments: Stripe
- Hosting: Fly.io or Railway
Stages:
| Stage | Trigger | Action |
|---|---|---|
| 0 | Now | No backend. Free works 100% offline. |
| 1 | Pro feature (Sync) | Backend MVP + Stripe integration |
| 2 | 1000+ Pro users | Sync, teams, dashboard |
Principles:
- Free tier never requires account
- Pro tier requires account for subscription management
- No DRM, no hardware fingerprinting
- Soft device limits (warning at 4, block at 5)
Full audit: See GitHub issue #28 and Claude plan file.
| Debt | Risk | Mitigation |
|---|---|---|
| No sync | Low | Schema has sync-ready fields |
| No mobile | Low | Core is portable |
| No plugins | Medium | Boundary defined, not implemented |
| Basic search | Low | FTS5 can be added later |
| No collaboration | Low | Single-user is the product |
| Rule | Description |
|---|---|
| Preload minimal | Only expose necessary APIs |
| IPC whitelist | Typed channels, no generic executeSQL |
| No nodeIntegration | Renderer is sandboxed |
| Schemas shared | packages/ipc-contract validates both sides |
Even without sync, include these from day 1:
interface NoteRecord {
id: string;
content: string; // Full Markdown
title: string;
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
deletedAt: string | null; // Soft delete
revision: number; // Optimistic locking
deviceId: string; // Future sync
}| Decision | v0.1 Choice | Rationale |
|---|---|---|
| Pagination | Cursor-based | Scales with large note counts, stable ordering |
| Full-text search | LIKE '%term%' |
Simple, good enough for <10k notes. FTS5 deferred. |
| Indices | updatedAt, deletedAt, title |
Sorted lists, soft-delete queries, search |
| Reindex | On save (sync) | Simplest model, no background workers |
| Batch operations | None | Single-note ops only in v0.1 |
Pagination pattern:
interface ListNotesQuery {
cursor?: string; // Last note ID from previous page
limit: number; // Default 50, max 100
sort: 'updatedAt' | 'createdAt' | 'title';
direction: 'asc' | 'desc';
filter?: {
deletedOnly?: boolean;
tag?: string;
};
}
// SQL example
const sql = `
SELECT * FROM notes
WHERE deletedAt IS NULL
AND updatedAt < ?
ORDER BY updatedAt DESC
LIMIT ?
`;Why not FTS5 in v0.1:
- Adds complexity (separate table, triggers)
- LIKE is fine for most users (<5k notes)
- FTS5 can be added later without migration (new table + backfill)
Future (v0.2+):
- FTS5 for full-text search
- Batch import/export
- Background indexing
Without signing and auto-update from day 1, the app doesn't exist as a product.
| Component | Decision | Rationale |
|---|---|---|
| CI | GitHub Actions | Free, integrated, battle-tested |
| Build | electron-builder | Standard for Electron apps |
| Artifacts | GitHub Releases | Free hosting, auto-update compatible |
| Secrets | GitHub Secrets | Secure, no extra infra |
| Platform | Requirement | Cost | Priority |
|---|---|---|---|
| macOS | Apple Developer ID + Notarization | $99/year | Day 1 |
| Windows | Code Signing Certificate | $100-200/year | Day 1 |
| Windows | EV Code Signing (SmartScreen trust) | $300-500/year | When traction |
macOS Config (electron-builder):
{
"mac": {
"hardenedRuntime": true,
"entitlements": "entitlements.mac.plist",
"entitlementsInherit": "entitlements.mac.plist",
"gatekeeperAssess": false,
"target": ["dmg"]
}
}| Decision | Choice | Rationale |
|---|---|---|
| Updater | electron-updater | Built-in, reliable |
| Feed | GitHub Releases | Free, simple |
| Policy | Mandatory from v0.1.0 | Bugs must be fixable |
import { autoUpdater } from 'electron-updater';
autoUpdater.checkForUpdatesAndNotify();El repo es privado, por lo que los releases requieren autenticación.
Configuración actual:
publish.private: trueen package.jsonGH_TOKENsecret con PAT de solo lectura- electron-updater maneja auth automáticamente
Riesgos aceptados:
- Token recuperable del binario (inevitable para repos privados)
- Si token expira → auto-update roto
- Dependencia de GitHub infra
Cuando el producto tenga usuarios pagos, churn tracking, y soporte activo, la arquitectura evoluciona a:
| Actual (Pre-revenue) | Futuro (Post-revenue) |
|---|---|
| Repo privado | Releases públicos (o CDN) |
| Updater con auth | Updater sin auth |
| N/A | Licencia validada aparte |
| N/A | Features gated por licencia |
Principio: Updates accesibles para todos. Monetización via licencia, no via acceso a updates.
Implementación futura:
- Mover releases a GitHub público o CDN propio
- Eliminar
private: truey token - Validar licencia al iniciar app
- Gating de features Pro via capabilities system
interface VersionInfo {
appVersion: string; // "0.3.2" - SemVer
domainVersion: number; // 2 - Core model version
schemaVersion: number; // 5 - SQLite schema version
}Rules:
- Forward-only migrations (never rollback schema)
- Automatic backup before any migration
- Never break user data on update
git tag v0.1.0
↓
GitHub Actions (matrix: mac + win)
↓
Build + Sign binaries
↓
Notarize (macOS)
↓
Publish to GitHub Releases
↓
Auto-update feed live
| Item | Annual Cost |
|---|---|
| Apple Developer Program | $99 |
| Windows Code Signing | $100-200 |
| GitHub Actions | Free tier |
| Total (minimum) | ~$200/year |
| Phase | Scope | Deliverable |
|---|---|---|
| 0 | This document | Architecture frozen |
| 1 | Bootstrap | Monorepo + packages/core with tests |
| 2 | Storage | packages/storage + SQLite + migrations |
| 3 | IPC | packages/ipc-contract + electron-vite setup |
| 4 | CI/Release | GitHub Actions + signing + auto-update |
| 5 | UI | packages/ui-kit + design tokens + basic shell |
| 6 | Editor | CodeMirror 6 integration |
| 7 | Integration | Full app working end-to-end |
| 8 | Polish | Error handling, edge cases, performance |
Documentation is part of the product, not an afterthought.
| Option | Verdict | Rationale |
|---|---|---|
| VitePress | ✅ Chosen | Vite-based, Markdown-native, versionable, simple |
| Docusaurus | ❌ | Too heavy, React everywhere, overkill for indie |
| Astro + MDX | ❌ | Overkill unless marketing + docs are merged |
apps/docs-site/
├── .vitepress/
│ ├── config.ts
│ └── sidebar.ts
├── guide/
│ ├── getting-started.md
│ └── principles.md
├── architecture/
│ ├── overview.md
│ ├── core.md
│ ├── application-layer.md
│ ├── storage.md
│ ├── ipc.md
│ ├── editor.md
│ └── theming.md
├── decisions/
│ ├── ADR-001-runtime-contract.md
│ ├── ADR-002-markdown-model.md
│ ├── ADR-003-sqlite.md
│ ├── ADR-004-css-system.md
│ └── ADR-005-monetization.md
├── operations/
│ ├── release-process.md
│ ├── signing.md
│ └── updates.md
└── roadmap/
├── mvp.md
├── v0.1.md
└── v0.2.md
| Category | Purpose |
|---|---|
| Guide | How to get started, principles |
| Architecture | How things work |
| Decisions (ADR) | Why things were chosen |
| Operations | How to distribute/release |
| Roadmap | What gets built and in what order |
Rule: Never mix categories. Architecture ≠ Decisions ≠ Operations.
# ADR-XXX: [Title]
## Status
Accepted | Superseded | Deprecated
## Context
What problem are we solving?
## Decision
What did we decide?
## Consequences
- Positive:
- Negative:
- Risks:
## Alternatives Considered
1. Option A — rejected because...
2. Option B — rejected because...You can't fix what you can't see.
| Decision | Choice | Rationale |
|---|---|---|
| Error capture | Sentry | Industry standard, good Electron support |
| Scope | Main + Renderer | Catch both process types |
| Privacy | No PII in payloads | User trust |
| What | Purpose | Consent |
|---|---|---|
| App version | Know adoption | Implicit |
| Crash reports | Fix bugs | Explicit opt-in |
| Feature usage | Prioritize roadmap | Explicit opt-in |
Rule: Telemetry is OFF by default. User must opt-in.
interface LogEntry {
level: 'debug' | 'info' | 'warn' | 'error';
timestamp: string;
context: string;
message: string;
data?: unknown;
}- Logs stored locally (rotating, max 10MB)
- User can export logs for support
- Never send logs without consent
Schema changes are inevitable. Plan for them from day 1.
| Version | Purpose | Location |
|---|---|---|
appVersion |
Release version (SemVer) | package.json |
schemaVersion |
SQLite schema version | migrations table |
domainVersion |
Note model structure | note record |
serializationVersion |
Markdown format version | note frontmatter |
interface NoteRecord {
// ... existing fields
domainVersion: number; // Current: 1
}Rules:
- Bump
domainVersionwhen Note structure changes - Old versions readable forever (forward compatibility)
- Migrations transform old → new on read
- Never break existing data
---
serializationVersion: 1
title: My Note
tags: [foo, bar]
---Rules:
- Frontmatter declares format version
- Parser handles all known versions
- Export always uses latest version
- Import handles older formats gracefully
interface DomainMigration {
from: number;
to: number;
migrate: (old: unknown) => NoteRecord;
canRollback: boolean;
}Rules:
- Migrations are pure functions
- Each migration tested independently
- Chain migrations for multi-version jumps
- Rollback only if explicitly safe
Zero lock-in. User data belongs to the user.
| Platform | Location |
|---|---|
| macOS | ~/Library/Application Support/Dripnex/ |
| Windows | %APPDATA%/Dripnex/ |
| Linux | ~/.config/Dripnex/ |
Structure:
Dripnex/
├── dripnex.db # SQLite database
├── backups/ # Automatic backups
├── logs/ # Application logs
└── config.json # User preferences
Primary: Markdown + JSON metadata
export/
├── notes/
│ ├── note-1.md
│ ├── note-2.md
│ └── ...
├── metadata.json # Tags, links, timestamps
└── manifest.json # Export version, date
Guarantees:
- Markdown files are valid, standalone
- Can be opened in any editor
- metadata.json preserves structure
- Re-importable to Dripnex
| Source | Support |
|---|---|
| Obsidian vault | Basic (read .md files) |
| Plain Markdown folder | Full |
| Other apps | Deferred |
Import process:
- Select folder
- Scan for .md files
- Parse frontmatter (if exists)
- Create notes with original timestamps (if available)
| Feature | v0.1 | Notes |
|---|---|---|
.md files |
✅ | Basic read, preserves content |
| YAML frontmatter | ✅ | tags, created, updated, aliases |
Wikilinks [[note]] |
✅ | Converted to internal links |
Wikilinks with alias [[note|display]] |
✅ | Preserves display text |
Headings [[note#heading]] |
Link preserved, heading jump deferred | |
Obsidian embeds ![[image.png]] |
❌ | Deferred to v0.2 |
| Attachments folder | ❌ | Deferred to v0.2 |
| Aliases (frontmatter) | Parsed, search support deferred | |
Tags in body #tag |
✅ | Extracted to tags array |
Nested tags #parent/child |
Flattened to parent-child |
|
| Daily notes | ✅ | Imported as regular notes |
| Templates folder | Imported as notes (no template behavior) | |
| Dataview queries | ❌ | Not interpreted, kept as code blocks |
| Obsidian plugins data | ❌ | Ignored (.obsidian/ folder) |
Legend:
- ✅ Fully supported
⚠️ Partially supported- ❌ Not supported (deferred)
- App removal does NOT delete data directory
- User must explicitly delete data
- Clear prompt during uninstall (if possible)
Offline-first means updates work offline too.
App starts
↓
Check schemaVersion
↓
Need migration? → Backup DB → Run migrations → Update version
↓
App ready
Automatic backup:
- Created before ANY schema change
- Stored in
backups/pre-migration-{version}-{timestamp}.db - Keep last 3 backups
- User can restore manually
| Scenario | Action |
|---|---|
| Migration succeeds | Continue normally |
| Migration fails | Restore backup, block app, show error |
| Corruption detected | Restore backup, notify user |
Rule: Never leave data in inconsistent state.
On every startup:
- Verify SQLite integrity (
PRAGMA integrity_check) - Verify schema version matches expected
- If mismatch → attempt recovery or block
Test the core, not the UI.
| Layer | Type | Tools | Priority |
|---|---|---|---|
| Core | Unit | Vitest | High |
| Use-cases | Integration | Vitest | High |
| Storage | Integration | Vitest + SQLite | Medium |
| IPC | Contract | Vitest + Zod | Medium |
| E2E | None (v0.x) | — | Deferred |
// Example: packages/core/__tests__/createNote.test.ts
describe('createNote', () => {
it('creates note with valid input', () => {});
it('rejects empty content', () => {});
it('generates unique id', () => {});
it('sets timestamps correctly', () => {});
});Rules:
- Pure functions, no mocks needed
- Fast (< 1ms per test)
- 100% coverage of domain logic
// Example: packages/storage/__tests__/noteRepository.test.ts
describe('NoteRepository', () => {
it('saves and retrieves note', () => {});
it('handles concurrent writes', () => {});
it('migrates schema correctly', () => {});
});Rules:
- Use real SQLite (in-memory)
- Test migrations explicitly
- Test edge cases (large notes, special chars)
// Verify schemas match between main and renderer
describe('IPC Contract', () => {
it('request schemas are valid', () => {});
it('response schemas are valid', () => {});
it('error schemas are valid', () => {});
});- E2E browser tests (Playwright, Cypress)
- Visual regression tests
- Performance benchmarks
- Accessibility audits
Reason: Overhead too high for solo dev. Revisit at v1.0.
Knowing what NOT to build is as important as knowing what to build.
| Feature | Status | Rationale |
|---|---|---|
| Real-time collaboration | Never (v1) | Different product |
| Cloud sync | Deferred | Focus on offline-first |
| Mobile app | Deferred | Desktop-first |
| Arbitrary plugins | Never | Security + maintenance |
| Block-based editing | Never | Markdown-first identity |
| Multi-user / teams | Never (v1) | Single-user product |
| AI features | Deferred | Not core value prop |
| Web version | Deferred | Desktop is the product |
| Attachments / images | Deferred (v0.2) | Adds complexity; markdown text first |
Image embeds (![[img]]) |
Deferred (v0.2) | Requires attachment handling |
| PDF export | Deferred | Not core value |
| Attachments in DB | Never | Always filesystem-based, never embedded blobs |
Every "no" protects:
- Development focus
- Codebase simplicity
- Maintenance burden
- Security surface
Non-goals reviewed at major versions:
- v0.x → v1.0: Revisit sync, mobile
- v1.x → v2.0: Revisit collaboration
Freemium + Subscription. Free tier forever. Pro for sync and power features.
| Decision | Choice | Rationale |
|---|---|---|
| Model | Freemium + Subscription | Free tier guarantees app always works |
| Free | $0 forever | Core features, unlimited notes, local-only |
| Pro | $2.99/mo or $29/year | Sync, priority support, early access |
| Trial | 14 days, Pro features | Let users evaluate Pro properly |
| Processor | Lemon Squeezy | Handles VAT, subscriptions |
Why this model:
- Free tier ensures notes are never held hostage
- Subscription funds continued development sustainably
- Users upgrade for convenience (sync), not necessity (access to their data)
- Aligns with offline-first principle: core functionality works without internet
User downloads Dripnex (free)
↓
Free tier: unlimited notes, full editor, local backup
↓
Optional: Start 14-day Pro trial
↓
Subscribe to Pro: $2.99/mo or $29/year
- Cloud sync (coming soon)
- Priority support
- Early access to new features
↓
Cancel anytime → revert to Free tier
- Keep all notes (they're local files)
- Lose Pro-only features
Key rules:
- Free tier NEVER stops working
- Free tier NEVER blocks editing or reading
- Notes are ALWAYS local Markdown files you control
- Pro features require active subscription
// In capability check (renderer or main)
function hasCapability(capability: string, license: LicenseState): boolean {
// Free tier capabilities are always available
const freeCapabilities = [
'notes.unlimited',
'notebooks.unlimited',
'search.basic',
'export.markdown',
'backup.local',
];
if (freeCapabilities.includes(capability)) {
return true;
}
// Pro capabilities require active subscription
if (license.status === 'pro_active' || license.status === 'pro_trial') {
return license.capabilities.includes(capability);
}
return false;
}{
"licenseVersion": 1,
"licenseId": "sub_8f3a1c",
"issuedTo": "user@email.com",
"plan": "pro",
"status": "pro_active",
"currentPeriodEnd": "2025-02-15",
"trialEnd": null,
"capabilities": [
"notes.unlimited",
"sync.cloud",
"links.backlinks",
"search.advanced",
"export.structured",
"import.folder",
"themes.custom",
"graph.view"
],
"signature": "BASE64_ED25519_SIGNATURE"
}Fields:
plan: "free" | "pro"status: "free" | "pro_trial" | "pro_active" | "pro_grace" | "pro_expired"currentPeriodEnd: When current billing period ends (Pro only)trialEnd: When trial ends (if on trial)capabilities: Features enabled for current plansignature: Ed25519 signature for offline validation
type LicenseStatus =
| 'free' // Free tier, core features
| 'pro_trial' // 14-day Pro trial
| 'pro_active' // Active Pro subscription
| 'pro_grace' // Subscription expired, grace period
| 'pro_expired'; // Grace period ended, reverts to free
interface AppLicenseState {
status: LicenseStatus;
plan: 'free' | 'pro';
trial?: {
startDate: string;
daysRemaining: number;
};
subscription?: {
currentPeriodEnd: string;
cancelAtPeriodEnd: boolean;
capabilities: Capability[];
};
}Free Tier (forever):
- Unlimited notes and notebooks
- Full Markdown editor
- Local backup and export
- Basic search
- Updates included
Pro Trial (14 days):
- All Pro features enabled
- No credit card required
- After expiry: reverts to Free tier
Pro Active:
- Cloud sync (coming soon)
- Advanced search
- Priority support
- Early access to new features
Rule: Never punish users. Free tier is fully functional. Pro adds convenience, not essentials.
type Capability =
| 'notes.basic' // create / edit / delete notes
| 'notes.unlimited' // no note limit
| 'links.backlinks' // automatic backlinks
| 'search.basic' // simple search
| 'search.advanced' // ranking, filters
| 'export.markdown' // export .md
| 'export.structured' // export md + metadata
| 'import.folder' // import md folder
| 'import.obsidian' // import Obsidian vault
| 'themes.custom' // advanced theming
| 'graph.view'; // visual graph
// In application layer (NOT in core)
function hasCapability(cap: Capability): boolean {
const state = getLicenseState();
if (state.status === 'trial') return true;
if (state.status === 'active' || state.status === 'active_expired') {
// Licensed users have full access forever
return state.license?.capabilities.includes(cap) ?? false;
}
if (state.status === 'unlicensed') {
// Unlicensed: basic features only
return cap === 'notes.basic' || cap === 'export.markdown';
}
return false;
}Critical rule: Core does NOT know about capabilities. Evaluated in application layer only.
User visits marketing site
↓
Purchases via Lemon Squeezy checkout ($79)
↓
Lemon Squeezy generates signed license with updatesUntil
↓
User receives license.json via email
↓
User opens app → imports license file
↓
App validates Ed25519 signature (offline)
↓
Full access forever + updates for 12 months
User's updatesUntil approaches
↓
App shows "Updates expiring soon" notice
↓
User clicks "Renew" → marketing site ($39)
↓
New license.json with extended updatesUntil
↓
User imports new license
↓
Updates extended for 12 more months
If user doesn't renew:
- App keeps working exactly the same
- Just won't receive newer versions
- Can renew anytime to get latest
packages/licensing/
├── src/
│ ├── capabilities.ts # Capability type + hasCapability()
│ ├── validator.ts # Ed25519 signature verification
│ ├── storage.ts # Store/retrieve license locally
│ ├── trial.ts # Trial state management (14 days)
│ ├── updates.ts # Update eligibility check
│ ├── types.ts # Interfaces
│ └── index.ts
├── __tests__/
│ ├── capabilities.test.ts
│ ├── validator.test.ts
│ ├── trial.test.ts
│ └── updates.test.ts
└── package.json
Static site. Astro. Lemon Squeezy checkout.
| Component | Choice | Rationale |
|---|---|---|
| Framework | Astro | Static, fast, simple |
| Styling | Tailwind | Consistent with app |
| Checkout | Lemon Squeezy embed | No backend needed |
| Hosting | Vercel | Free, fast, simple |
apps/marketing/
├── src/
│ ├── pages/
│ │ ├── index.astro # Landing page
│ │ ├── pricing.astro # Pricing + checkout
│ │ ├── download.astro # Download links
│ │ └── changelog.astro # Release notes
│ ├── components/
│ │ ├── Hero.astro
│ │ ├── Features.astro
│ │ ├── PricingCard.astro
│ │ └── Footer.astro
│ ├── layouts/
│ │ └── Base.astro
│ └── styles/
│ └── global.css
├── public/
│ ├── screenshots/
│ └── favicon.ico
├── astro.config.mjs
├── tailwind.config.js
└── package.json
| Page | Purpose |
|---|---|
/ |
Landing: hero, features, social proof |
/pricing |
Pricing card + Lemon Squeezy checkout |
/download |
Download links (Mac, Windows) |
/changelog |
Release notes (can pull from GitHub) |
Marketing site develops in parallel with core:
| Core Phase | Marketing Task |
|---|---|
| Phase 1 (Core) | Setup Astro, basic landing |
| Phase 2 (Storage) | Features section, screenshots |
| Phase 3 (IPC) | Pricing page, Lemon Squeezy integration |
| Phase 4 (Desktop) | Download page, final polish |
Rule: Marketing site should be ready when desktop shell works.
- Architecture document complete
- Note model decided (Markdown source of truth)
- Tech stack frozen
- Boundaries defined
- Release & distribution strategy defined
- Documentation system defined (VitePress)
- Observability strategy defined
- Domain versioning strategy defined
- Data ownership & portability defined
- Offline upgrade path defined
- Testing strategy defined
- Non-goals documented
- Monetization strategy defined (subscription, Lemon Squeezy, capabilities)
- Marketing site planned (Astro in monorepo)
- Apple Developer account created
- Windows signing cert obtained
- Bootstrap monorepo
- CI pipeline working
- First core tests passing
- SQLite schema created
- Domain migrations tested
- Data directory setup (OS-specific paths)
- Backup system implemented
- Export (Markdown + JSON) working
- Basic Obsidian import working
- VitePress docs-site scaffolded
- ADR-001 (runtime contract) written
- ADR-002 (markdown model) written
- ADR-003 (storage) written
- Sentry integration configured
- Local logging implemented
- Integrity check on startup
- Lemon Squeezy account created
- Subscription product configured (Pro tier)
- Capabilities system implemented
- License validation package implemented
- Trial mode implemented (14 days)
- Grace period implemented (read-only mode)
- hasCapability() in application layer
- "Enter License" UI in app
- ADR-006 (monetization) written
- Astro project scaffolded
- Landing page designed
- Pricing page with Lemon Squeezy checkout
- Download page
- Deployed to Vercel
No time estimates. Just clear milestones. Marketing site runs as a parallel track.
| Phase | Milestone | Done When |
|---|---|---|
| 0 | Bootstrap | pnpm install works, CLAUDE.md exists |
| 1 | Core | pnpm test passes with note CRUD |
| 2 | Storage | Notes persist in SQLite |
| 3 | Desktop | Window opens, loads notes |
| 4 | Editor | Can create/edit/save markdown |
| 5 | Features | Search, backlinks, tags work |
| 6 | Release | Signed app downloadable |
Parallel: Marketing site ready when Phase 3 completes.
Goal: Working monorepo with docs.
Deliverables:
pnpm installruns without errorsCLAUDE.mdexists at rootplan.mdcompletedocs/marketing-content.mdready
Structure:
dripnex/
├── apps/
│ └── .gitkeep
├── packages/
│ └── .gitkeep
├── docs/
│ └── marketing-content.md
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
├── tsconfig.base.json
├── CLAUDE.md
├── plan.md
├── .gitignore
└── .nvmrc
Goal: Domain logic testable in pure Node.
Deliverables:
packages/corewith Note entity- Operations: createNote, updateNote, deleteNote
- Tests passing with Vitest
Done when: pnpm --filter @dripnex/core test passes.
Goal: SQLite adapter with migrations.
Deliverables:
packages/storagewith better-sqlite3- NoteRepository implementing core ports
- Migration runner + initial schema
Done when: Notes CRUD works against real SQLite.
Goal: Electron app that opens.
Deliverables:
apps/desktopwith electron-vite- Main process with SQLite
- Preload with IPC bridge
- Renderer with React
Done when: App opens, loads notes from SQLite.
Parallel: Marketing site should be ready now.
Goal: CodeMirror 6 editing notes.
Deliverables:
- CodeMirror 6 integration
- NoteEditor component
- Save on change
- Markdown syntax highlighting
Done when: Can create, edit, and save notes.
Goal: MVP feature set.
Deliverables:
- Note list (sidebar)
- Search (LIKE-based)
- Tags (from frontmatter)
- Backlinks (wikilinks)
- Keyboard shortcuts
Done when: Usable as daily note app.
Goal: Signed, auto-updating app.
Deliverables:
- GitHub Actions workflow
- electron-builder config
- Code signing (Mac + Windows)
- Auto-update feed
Done when: v0.1.0 downloadable and installable.
Product name: Dripnex
NPM scope: @dripnex/*
GitHub: https://github.com/dripnex/readide.git (consider renaming to dripnex)
Clone:
git clone https://github.com/dripnex/readide.git dripnex
cd dripnex
pnpm install