Case study
Разбор проекта
Structura.Wiki
A library for AI conversations that never leaves your browser. Import ChatGPT, Claude, or DeepSeek exports; browse them as books on a shelf — with no server in the loop, ever.
Библиотека для AI-диалогов, которая никогда не покидает браузер. Импортируйте экспорты ChatGPT, Claude или DeepSeek и листайте их как книги на полке — без сервера.
ReactViteTypeScriptDexie / IndexedDBTailwindVercel
01 · Context
01 · Контекст
Your conversation history, exported and then abandoned
Every major AI chat product lets you export your history — a ZIP file, downloaded once, then almost always left in Downloads forever. The export is real data, often months of thinking-out-loud, but it's locked inside a raw JSON structure never meant for a human to read directly, and there's no tool built to make it browsable. Nobody wants to upload months of private conversations to a third-party service just to read them comfortably.
Structura turns that export into a library: each conversation becomes a book, organized onto shelves, taggable with stickers. The interesting constraint is self-imposed — for a tool whose entire input is "my private conversations," a server in the request path isn't a minor privacy concern, it's a disqualifying one. So the product has no backend. Import, storage, browsing, search, export — all of it happens in the tab, in IndexedDB, and nowhere else.
Licensing follows the same logic: the code is source-available under FSL-1.1-ALv2, converting to Apache 2.0 after two years. Not MIT, because a source-available license still discourages a direct SaaS relaunch of the exact product during the window that matters most — but source-available regardless, because a tool asking to be trusted with your private conversations should let anyone verify the "nothing leaves the browser" claim by reading the code, not just by reading a promise.
История диалогов, выгруженная и забытая
Любой крупный AI-чат позволяет выгрузить историю — ZIP-файл, скачанный один раз и почти всегда навсегда оставшийся в папке загрузок. Экспорт — реальные данные, часто месяцы размышлений вслух, но они заперты в сыром JSON, не рассчитанном на прямое чтение человеком, а инструмента, делающего его просматриваемым, попросту нет. Никто не хочет загружать месяцы личных переписок в сторонний сервис лишь ради удобного чтения.
Structura превращает этот экспорт в библиотеку: каждый диалог становится книгой, книги раскладываются по полкам, помечаются стикерами. Интересное ограничение здесь — самоналоженное: для инструмента, весь ввод которого — «мои личные переписки», сервер на пути запроса — не мелкая забота о приватности, а дисквалифицирующая. Поэтому у продукта нет бэкенда. Импорт, хранение, просмотр, поиск, экспорт — всё происходит во вкладке, в IndexedDB, и больше нигде.
Лицензирование следует той же логике: код source-available под FSL-1.1-ALv2, переходящей в Apache 2.0 через два года. Инструмент, просящий доверить ему личные переписки, должен позволять любому проверить заявление «ничего не покидает браузер» чтением кода, а не только чтением обещания.
02 · Requirements & constraints
02 · Требования и ограничения
What it must do — and under what conditions
| FR | Import a ZIP export from ChatGPT, Claude, or DeepSeek. Detect which platform produced it automatically — no format picker. Render each conversation as a readable "book"; organize books onto shelves; tag with stickers; search across the library; export back out. |
|---|---|
| NFR | Zero network requests, at any point in the application lifecycle after the page has loaded — not "we don't log," but structurally incapable of phoning home. Must handle exports large enough to contain hundreds of conversations without freezing the UI thread. |
| Constraints | No backend is permitted, by requirement — not a cost-saving choice, an inversion of the usual NFR hierarchy where "add a server" is normally the easy answer. Three undocumented export formats, none with a public spec, all of which drift silently between platform releases. |
Что система обязана делать — и в каких условиях
| FR | Импортировать ZIP-экспорт из ChatGPT, Claude или DeepSeek. Автоматически определять платформу-источник — без выбора формата вручную. Отображать каждый диалог как читаемую «книгу»; раскладывать книги по полкам; помечать стикерами; искать по всей библиотеке; экспортировать обратно в виде MD файла для реинтеграции и контекст менеджмента. |
|---|---|
| NFR | Ноль сетевых запросов в любой момент жизненного цикла приложения после загрузки страницы — не «мы не логируем», а структурная неспособность «звонить домой». Обработка экспортов, содержащих сотни диалогов, без подвисания UI-потока. |
| Ограничения | Бэкенд запрещён по требованию — не решение ради экономии, а инверсия обычной иерархии NFR, где «добавить сервер» обычно лёгкий ответ. Три недокументированных формата экспорта, ни один без публичной спецификации, все тихо дрейфуют между релизами платформ. |
03 · Architecture
03 · Архитектура
One request, ever
Vercel serves the static build — HTML, JS, CSS. That's the entire role of the network in this system: deliver the app once. Everything after that page load is local — a Web Worker unzips and parses the export off the main thread, a detection layer fingerprints which platform produced it, an adapter normalizes it into a common shape, and Dexie writes the result into IndexedDB. No component in this diagram has a line pointing back to a server, because none exists to point to.
Один запрос, и всё
Vercel раздаёт статическую сборку — HTML, JS, CSS. Это вся роль сети в системе: доставить приложение один раз. Всё, что происходит после загрузки страницы, — локально: Web Worker распаковывает и парсит экспорт вне основного потока, слой определения формата снимает отпечаток источника, адаптер приводит данные к общей форме, а Dexie записывает результат в IndexedDB. Ни у одного компонента на диаграмме нет стрелки обратно к серверу — потому что сервера нет.
04 · Key decisions
04 · Ключевые решения
Four decisions that shaped the system
Digests of the choices that mattered. Each names what the decision cost — a trade-off you don't acknowledge is a trade-off you didn't see.
Четыре решения, определившие систему
Выжимки из значимых решений. В каждой названа цена — трейд-офф, который не назван, это трейд-офф, который не был замечен.
No backend, structurally — not by policy
Без бэкенда структурно — не по политике
- Options
- A thin backend for parsing and cross-device sync · client-side only, static hosting.
- Chosen
- Client-side only. There is no server component to the product at all.
- Why
- "We don't log your data" is a policy — trustworthy only as far as the reader trusts the company. "There is no server that could receive your data" is a structural fact, verifiable by reading the network tab or the source. For a tool whose entire input is private conversation history, the structural guarantee is the only one worth building.
- What it cost
- No cross-device sync, no server-side full-text indexing at scale, and no way to recover a user's library if their browser storage is cleared — the backup story is entirely the user's export button.
- Варианты
- Тонкий бэкенд для парсинга и синхронизации между устройствами · только клиент, статический хостинг.
- Выбрано
- Только клиент. У продукта нет серверной части вообще.
- Почему
- «Мы не логируем ваши данные» — это политика, заслуживающая доверия ровно настолько, насколько читатель доверяет компании. «Сервера, способного получить ваши данные, не существует» — структурный факт, проверяемый чтением вкладки сети или исходников. Для инструмента, весь ввод которого — личная история переписки, структурная гарантия — единственная, которую стоит строить.
- Цена
- Нет синхронизации между устройствами, нет серверного полнотекстового индекса на масштабе, нет способа восстановить библиотеку пользователя при очистке хранилища браузера — весь бэкап держится на кнопке экспорта.
Content fingerprint, not filename or user choice
Отпечаток содержимого, а не имя файла или выбор пользователя
- Options
- Ask the user which platform the export came from · detect by filename pattern · detect by structural fingerprint of the parsed JSON.
- Chosen
- Structural fingerprint. Each adapter inspects the parsed structure — key names, nesting shape, ID formats — and claims the file only if it matches.
- Why
- Filenames get renamed constantly; asking the user is friction on the very first action they take, and users often don't reliably know which export is which once files pile up in Downloads. Content is the only signal that can't be accidentally wrong.
- What it cost
- The detector has to partially parse a file before it can decide how to parse it — a two-pass cost on every import. And it's the piece most exposed to breaking silently: a platform changing its export shape doesn't throw an error, it just fails to match, and needs its own test fixture to catch.
- Варианты
- Спросить у пользователя, откуда экспорт · определять по шаблону имени файла · определять по структурному отпечатку распарсенного JSON.
- Выбрано
- Структурный отпечаток. Каждый адаптер осматривает распарсенную структуру — имена ключей, форму вложенности, формат ID — и забирает файл, только если тот совпадает.
- Почему
- Имена файлов постоянно переименовывают; спрашивать пользователя — трение на самом первом действии, и пользователи часто не помнят точно, какой экспорт откуда, когда файлы накапливаются в загрузках. Содержимое — единственный сигнал, который нельзя случайно перепутать.
- Цена
- Детектору приходится частично распарсить файл, прежде чем решить, как его парсить, — двухпроходная стоимость на каждый импорт. И это самое уязвимое к тихой поломке место: смена формата экспорта платформой не бросает ошибку, а просто перестаёт совпадать, и это нужно ловить отдельным тестовым фикстуром.
Source-available, not MIT
Открытые исходники, но не MIT
- Options
- MIT · fully closed source · Functional Source License (FSL-1.1-ALv2).
- Chosen
- FSL-1.1-ALv2 — source visible immediately, converting to Apache 2.0 after two years.
- Why
- Closed source undermines the entire pitch: a privacy claim nobody can verify is just a promise again. MIT verifies the claim but leaves zero protection if a well-funded competitor forks it into a hosted SaaS the following week. FSL gets the verifiability of open source now, with a two-year runway before the code becomes fully permissive.
- What it cost
- FSL isn't OSI-approved, so it's invisible to tooling and directories that filter by "open source" license, and it requires explaining to anyone who assumes "source-available" means "MIT."
- Варианты
- MIT · полностью закрытый код · Functional Source License (FSL-1.1-ALv2).
- Выбрано
- FSL-1.1-ALv2 — исходники видны сразу, переход в Apache 2.0 через два года.
- Почему
- Закрытый код подрывает саму идею: непроверяемое заявление о приватности — снова просто обещание. MIT подтверждает заявление, но не даёт никакой защиты, если хорошо профинансированный конкурент форкнёт продукт в SaaS на следующей неделе. FSL даёт проверяемость открытого кода сейчас, с двухлетним запасом до полностью пермиссивной лицензии.
- Цена
- FSL не одобрена OSI, поэтому невидима для тулинга и каталогов, фильтрующих по «open source», и требует объяснения всем, кто решит, что «исходники открыты» значит «MIT».
Dexie over raw IndexedDB
Dexie вместо сырого IndexedDB
- Options
- Raw IndexedDB API · localForage · Dexie.js.
- Chosen
- Dexie — a thin, promise-based, queryable layer directly over IndexedDB.
- Why
- Raw IndexedDB's event-driven API turns a simple "find books on this shelf, sorted by date" into boilerplate that obscures intent. Dexie gives relational-feeling queries across books, shelves, and stickers while adding zero network surface — it's a client-only convenience layer, not a dependency that compromises the privacy model.
- What it cost
- One more third-party dependency in a codebase whose entire pitch is "trust what's running in your browser" — mitigated by Dexie being small, storage-only, and auditable in an afternoon, but a purist would still ask why it's there.
- Варианты
- Сырой IndexedDB API · localForage · Dexie.js.
- Выбрано
- Dexie — тонкий, promise-based, запрашиваемый слой прямо над IndexedDB.
- Почему
- Событийный API сырого IndexedDB превращает простое «найти книги на этой полке, отсортировать по дате» в шаблонный код, скрывающий намерение. Dexie даёт запросы, ощущающиеся реляционными, по книгам, полкам и стикерам, не добавляя сетевой поверхности — это клиентский слой удобства, а не зависимость, компрометирующая модель приватности.
- Цена
- Ещё одна сторонняя зависимость в кодовой базе, вся идея которой — «доверяй тому, что работает в твоём браузере», — смягчается тем, что Dexie маленькая, только для хранения и аудируется за вечер, но пурист всё равно спросит, зачем она здесь.
05 · Data model
05 · Модель данных
Books, shelves, stickers
Four Dexie tables hold the library: books (one per imported conversation — source platform, title, timestamps, and a normalized array of messages produced by whichever adapter claimed the file), shelves (user-defined collections), stickers (tags), and a join table linking books to both. The messages array is the payload every adapter converges on — regardless of whether the source used a flat list, like ChatGPT's, or a branching tree of edits and regenerations, the adapter's job is to resolve it down to the same linear shape the reader component expects.
Книги, полки, стикеры
Четыре таблицы Dexie держат библиотеку: books (одна на импортированный диалог — платформа-источник, заголовок, таймстемпы и нормализованный массив сообщений, полученный от адаптера, забравшего файл), shelves (пользовательские коллекции), stickers (теги) и связующая таблица между книгами и обоими. Массив сообщений — общая форма, к которой сходятся все адаптеры: независимо от того, был ли источник плоским списком, как у ChatGPT, или ветвящимся деревом правок и перегенераций, задача адаптера — свести его к той же линейной форме, которую ожидает компонент чтения.
06 · Hard problems
06 · Трудные места
Three formats, zero documentation
None of the three platforms publish an export schema. Reverse-engineering meant exporting real conversations from each, then reading the raw JSON by hand to find the shape. The formats turned out to disagree on the one thing that mattered most: how a conversation is structured at all. ChatGPT's export is a tree — every message has a parent pointer, because edits and regenerations create branches, and the "current" conversation is really one path through that tree that has to be walked and flattened. Claude's export is closer to a flat ordered list. DeepSeek's sits somewhere between the two, with its own vocabulary for the same concepts. A ConversationAdapter interface absorbs the difference: each adapter's only contract is to take its platform's raw shape in and hand back the same normalized message array, regardless of how different the walk was to get there.
Formats move under you
An undocumented format isn't a fixed target — it's a snapshot of whatever the platform happened to export the week you looked. A field renamed or nested one level differently in a later export doesn't throw a parse error; the content-fingerprint detector simply fails to recognize the file, or worse, a loosely-written check matches it to the wrong adapter and silently produces garbled output. The mitigation is defensive fingerprinting — checking for several structural markers together rather than one loose signal — plus keeping a real anonymized export from each platform as a permanent test fixture, so a platform-side format change shows up as a failing test instead of a support ticket.
Три формата, ноль документации
Ни одна из трёх платформ не публикует схему экспорта. Реверс-инжиниринг означал экспортировать реальные диалоги с каждой, а затем читать сырой JSON вручную, чтобы понять форму. Форматы разошлись в самом важном: в том, как вообще устроен диалог. Экспорт ChatGPT — дерево: у каждого сообщения есть указатель на родителя, потому что правки и перегенерации создают ветви, а «текущий» диалог — на самом деле один путь по этому дереву, который нужно пройти и развернуть в линию. Экспорт Claude ближе к плоскому упорядоченному списку. DeepSeek — где-то между, с собственным словарём для тех же понятий. Интерфейс ConversationAdapter поглощает разницу: единственный контракт каждого адаптера — принять сырую форму своей платформы и вернуть тот же нормализованный массив сообщений, независимо от того, насколько разным был путь туда.
Форматы двигаются под ногами
Недокументированный формат — не фиксированная мишень, а снимок того, что платформа случайно экспортировала на той неделе, когда вы смотрели. Переименованное поле или сдвинутая на уровень вложенность в более позднем экспорте не бросает ошибку парсинга — детектор по отпечатку содержимого просто не узнаёт файл, а хуже того, слабо написанная проверка отдаёт его не тому адаптеру и тихо производит искажённый вывод. Смягчение — защитное снятие отпечатка: проверка нескольких структурных маркеров вместе, а не одного слабого сигнала, — плюс хранение реального обезличенного экспорта с каждой платформы как постоянного тестового фикстура, чтобы изменение формата на стороне платформы проявлялось падающим тестом, а не тикетом в поддержку.
07 · Verification
07 · Верификация
The privacy claim was proven, not asserted
"Nothing leaves your browser" is the entire value proposition, so it's the one claim on this page that doesn't get to rest on code review alone. It was checked three separate ways, each catching a different failure mode:
Заявление о приватности доказано, а не заявлено
«Ничего не покидает браузер» — вся ценность продукта, поэтому это единственное заявление на этой странице, которому нельзя опираться только на код-ревью. Оно проверено тремя разными способами, каждый ловит свой класс отказа:
1 · Static audit
1 · Статический аудит
A grep across the source for every network-capable call — fetch, XMLHttpRequest, WebSocket, navigator.sendBeacon, <img src> pointed off-origin — turned up nothing outside build tooling and font preconnects in index.html, neither of which touches conversation data.
Grep по исходникам на все сетевые вызовы — fetch, XMLHttpRequest, WebSocket, navigator.sendBeacon, <img src> на чужой origin — не нашёл ничего вне билд-тулинга и preconnect для шрифтов в index.html, ни то ни другое не касается данных диалогов.
2 · Empirical network capture
2 · Эмпирический захват сети
DevTools' Network tab, recording continuously through a full session: import a real ZIP export, browse a dozen books, tag several with stickers, run a search, export the library back out. Result: one request on initial page load, zero after — confirming what the static audit predicted, against actual runtime behavior rather than just the code that should produce it.
Вкладка «Сеть» DevTools, непрерывная запись через полную сессию: импорт реального ZIP-экспорта, просмотр десятка книг, разметка стикерами, поиск, экспорт библиотеки обратно. Результат: один запрос при начальной загрузке страницы, ноль после — подтверждение того, что предсказал статический аудит, но по реальному поведению в рантайме, а не только по коду, который должен его производить.
3 · Browser-enforced, not just code-reviewed
3 · Гарантировано браузером, а не только код-ревью
A Content-Security-Policy header with connect-src 'self' makes the guarantee structural rather than behavioral: even a future dependency that tried to make an outbound call would be blocked by the browser itself and surfaced as a console error, not silently shipped. The three checks compose deliberately — static audit catches what's written, network capture catches what actually runs, and CSP catches what might get written later without anyone noticing.
Заголовок Content-Security-Policy с connect-src 'self' делает гарантию структурной, а не поведенческой: даже будущая зависимость, попытавшаяся сделать исходящий вызов, будет заблокирована самим браузером и всплывёт как ошибка в консоли, а не тихо уйдёт в прод. Три проверки намеренно дополняют друг друга: статический аудит ловит то, что написано, захват сети — то, что реально выполняется, а CSP — то, что может быть написано позже и остаться незамеченным.
08 · Retrospective
08 · Ретроспектива
If I rebuilt it
- The CSP header should have existed from the first commit, not been added as a verification afterthought. A structural guarantee is stronger when it's been true the whole time, not just true now.
- Each adapter's test fixture should be a contract, checked in CI against a fresh export whenever one of the source platforms ships an update — currently that check is manual and triggered by noticing something looks wrong.
- An in-browser export-your-library-as-a-file backup prompt, surfaced periodically. With no server, "clear site data" is unrecoverable, and the product doesn't currently warn anyone about that trade-off at the moment it matters.
- The two-year FSL conversion window is a decision I'd want to revisit closer to the deadline, not one to treat as settled — the right call depends on how the project's usage looks by then.
Если бы строил заново
- Заголовок CSP должен был существовать с первого коммита, а не быть добавленным задним числом как элемент верификации. Структурная гарантия сильнее, когда она была истинной всё время, а не только сейчас.
- Тестовый фикстур каждого адаптера должен быть контрактом, проверяемым в CI против свежего экспорта при каждом обновлении платформы-источника — сейчас эта проверка ручная и запускается, когда что-то показалось неладным.
- Периодическая подсказка в браузере — экспортировать библиотеку в файл-бэкап. Без сервера «очистить данные сайта» невосстановимо, а продукт пока не предупреждает об этом в момент, когда это важно.
- Двухлетнее окно перехода FSL — решение, которое я бы пересмотрел ближе к сроку, а не считал закрытым: правильный выбор зависит от того, как будет выглядеть использование проекта к тому моменту.