Stashio — saving the internet without losing your mind

Stashio — lưu internet không mất trí

Stashio is the odd app in the uranashel portfolio. Wheria, Estua, Sonarish and Phyzix treat the phone as a self-contained instrument: sensors in, processed numbers out, nothing leaves the device unless you export it yourself. Stashio breaks that pattern deliberately. The problem it solves is accumulation across time and devices. You find a PDF on your phone, a link on your laptop, a voice memo while walking, a screenshot of a parking pillar number at midnight (mine read E9, floor B2), and three weeks later you need "that article about barometer math" without remembering the exact title, the site or which app swallowed it. The article, for the record, was this one. The core job splits into three verbs: capture from anywhere, organize into named Spaces, retrieve by keyword or by meaning. Cloud sync sits on top as an optional enhancement. It never becomes the gate.

Capture gets a 3-second budget

The primary entry point is the OS share sheet, the system UI that appears when you tap Share in Safari, Chrome, a PDF viewer or a photo gallery. Every extra decision at that moment costs saves. So the design goal at capture time is zero friction: no mandatory tags, no folder picking, no "where should this go?" modal that makes you abandon the whole idea. You share, it stashes, you move on.

On iOS that politeness has to survive a hostile runtime. Stashio registers a Share Extension, which the system runs as a separate lightweight process with a memory ceiling we have measured being enforced near 120 MB. The extension therefore does the minimum: it copies the shared item into an app group container, writes one metadata row and exits. Indexing waits until the main app next opens and gets signalled that new items are queued. On Android, Stashio registers as a Share Target in the manifest. Small items import inline. A large PDF triggers a foreground service so the system does not kill the import mid-write, and the extracted metadata lands in a Room database entry once the copy completes. From our July bench notes (3 iPhones, 3 Android phones, release builds, 200 shares each): median share-to-saved time was 1.9 s for a URL and 4.8 s for a 40 MB PDF.

Extraction stays on the device

Five input types are supported. URLs arrive with automatic title and preview extraction. PDF documents get on-device text extraction where the OS provides it, which covers most digitally-produced files. Images, including camera scans of receipts, run through the platform OCR path (Vision on iOS, the on-device ML Kit recognizer on Android); a typical receipt photo gives up its text in about 300 ms on an iPhone 12. Selected text fragments store as-is. Voice memos can be recorded inside the app or shared over from a recorder. Everything extracted feeds one local search index. None of it needs a network round trip, which matters when the thing being saved is a pillar screenshot three floors below the cell grid.

Spaces are sets, files are singletons

Organization happens after capture. Items live in Spaces: user-created collections like "Garage QA logs," "DSP papers," or "Recipe links". One item can belong to several Spaces at once without duplicating the underlying file. Internally a Space membership is a row in a join table; the blob exists exactly once on disk, reference-counted, so removing an item from its last Space is the only delete that ever touches storage. This sounds like database trivia until you watch someone file a manufacturer datasheet under both "DSP papers" and a project Space, then expect an annotation made in one place to appear in the other. It does, because there is only one item. Keyword search runs against titles, user-added tags and extracted text bodies, with prefix matching so "barom" surfaces the barometer material before you finish typing.

Search by meaning, still offline

Keywords fail in one specific way: you remember the concept but forget the vocabulary. For that case Stashio optionally computes an embedding vector for every item, on the device, and indexes the vectors locally. A query like "that thing about hydrostatic pressure in parking garages" can match a saved blog draft even if the word "hydrostatic" never appeared in your tags. The model is a small sentence encoder producing 384-dimensional vectors; similarity is plain cosine, cos θ = (a·b)/(|a||b|), and at personal-library scale nothing cleverer is required. My 2,400-item library stores its full vector index in about 3.7 MB (2,400 × 384 floats × 4 bytes), and a brute-force scan over all of it finishes in under 2 ms on an iPhone 12. We benchmarked an approximate-nearest-neighbor structure anyway, then discarded it: at this scale ANN saved microseconds and charged an index rebuild on every edit.

Ranking blends both signals:

// per query, all local
kw_scores  = bm25(query, titles + tags + bodies)
q_vec      = embed(query)            // 384-dim, on-device
sem_scores = {}
for item in library:                 // 2,400 iterations, under 2 ms
    sem_scores[item] = cosine(q_vec, item.vec)
final(item) = 0.6 * norm(kw_scores[item])
            + 0.4 * norm(sem_scores[item])
return top_k(final, 20)

The 0.6/0.4 split came from ktuyen's relevance spreadsheet rather than from theory. She scored 60 test queries against both extremes and the blend, and pure semantic search kept ranking a receipt for waterproof boots above an actual fluid-dynamics paper.

Why this app may touch the cloud

Bookmarking does not need 10 ms latency. Nobody's sleep depends on a share extension finishing inside an audio buffer deadline; that world belongs to the audio thread, where 10.7 ms is a hard wall. What a save-later tool does need is multi-device sync. Save on the phone, find on the tablet; that is the feature people actually pay for. So atuan built a small backend, with the same privacy instincts as the rest of the studio. The local cache is always readable offline. The account is optional. For embedding sync the server stores encrypted blobs, not plaintext content. Capture and search work without ever signing in, because the index is built on-device first and remains searchable in airplane mode. Our sensor apps run everything locally because physics demands it; Stashio keeps a cloud lane open because user workflow demands it. Same decision, opposite ends — the on-device-first post walks the whole spectrum.

The 2,400-link stress test

Before Stashio existed, my Apple Notes contained approximately 2,400 unsorted links accumulated over four years of garage testing, DSP paper reading and random internet detours. That pile became the seed corpus. After three months of daily use, roughly 94% of my retrieval attempts succeed in under 10 s, by keyword or by meaning — a number that comes from an actual log, since the app records locally whether each search session ended in an open or an abandon. The remaining 6% are honest failures, mostly items whose extracted text came back empty: screenshots of screenshots, largely. atuan uses Stashio for the API documentation snippets he needs during backend work. ktuyen attaches test-case screenshots and regression logs to Spaces before each release cycle, which means the QA history of every uranashel release now lives inside one of the apps being QA'd. Nobody planned that.

Same studio fingerprints

Stashio is not a physics instrument, and no Kalman filter hides in its settings screen. It still looks like us. The same monochrome UI language, the same Nunito on every label, the same 12 px corner radius, the same bilingual EN/VI strings that ktuyen reviews line by line. The design system is documented in the monochrome post, and it transferred to a bookmarking app without modification — we take that as evidence it was a system rather than a coincidence. The app page is here. Stashio solves a different class of problem from Wheria, and that difference is exactly why it lives in the portfolio alongside Wheria instead of replacing it: the sensor apps prove the phone can be an instrument, and Stashio proves the same three people can respect a completely different set of constraints when the problem asks for them.

Stashio là app lạc loài trong danh mục uranashel. Wheria, Estua, Sonarish và Phyzix coi điện thoại như một dụng cụ đo khép kín: cảm biến vào, số liệu đã xử lý ra, không gì rời máy trừ khi bạn tự export. Stashio cố tình phá luật đó. Bài toán nó giải là tích lũy qua thời gian và qua thiết bị. Bạn gặp một file PDF trên điện thoại, một link trên laptop, một đoạn ghi âm lúc đi bộ, một tấm screenshot số cột hầm xe lúc nửa đêm (của mình là E9, tầng B2). Ba tuần sau bạn cần tìm "bài viết về toán áp kế" mà chẳng nhớ title, nhớ site hay nhớ app nào đã nuốt nó. (Bài đó là bài này.) Việc chính gói trong ba động từ: capture từ mọi nơi, sắp xếp vào Spaces có tên, truy hồi bằng keyword hoặc bằng nghĩa. Sync cloud nằm trên cùng như một lớp tăng cường tùy chọn. Nó không bao giờ là cổng chặn.

Capture chỉ có 3 giây

Cửa vào chính là share sheet của hệ điều hành, cái UI hiện ra khi bạn bấm Share trong Safari, Chrome, viewer PDF hay gallery ảnh. Mỗi quyết định thừa ở khoảnh khắc đó đều làm mất lượt lưu. Nên mục tiêu thiết kế lúc capture là ma sát bằng không: không bắt gắn tag, không bắt chọn folder, không có cái modal "lưu vào đâu?" khiến bạn bỏ cuộc. Share xong, app stash, bạn đi tiếp.

Trên iOS, phép lịch sự đó phải sống sót qua một runtime khắc nghiệt. Stashio đăng ký Share Extension, được hệ thống chạy như một process nhẹ riêng. Theo đo đạc của team, trần bộ nhớ bị siết quanh 120 MB. Extension vì vậy chỉ làm phần tối thiểu: copy item vào app group container, ghi một dòng metadata rồi thoát. Việc index chờ tới lần mở app chính kế tiếp, khi app nhận tín hiệu có hàng mới trong queue. Trên Android, Stashio đăng ký Share Target trong manifest. Item nhỏ import ngay tại chỗ. PDF lớn kích một foreground service để hệ thống không kill giữa lúc đang ghi file, còn metadata trích ra rơi vào một entry Room database khi copy xong. Sổ bench tháng 7 của team (3 iPhone, 3 máy Android, build release, mỗi máy 200 lượt share): thời gian từ share đến đã-lưu trung vị là 1,9 s cho URL và 4,8 s cho một file PDF 40 MB.

Trích xuất nằm ngay trên máy

App nhận năm loại đầu vào. URL về kèm title và preview tự động. PDF được trích text on-device ở những chỗ OS hỗ trợ, đủ cho phần lớn tài liệu sinh ra từ máy tính. Ảnh, kể cả ảnh chụp hóa đơn, đi qua đường OCR của nền tảng (Vision trên iOS, bộ nhận dạng on-device của ML Kit trên Android); một tấm hóa đơn thường nhả hết chữ trong khoảng 300 ms trên iPhone 12. Đoạn text bôi chọn thì lưu nguyên trạng. Ghi âm có thể thu trong app hoặc share từ app recorder khác sang. Mọi thứ trích được đổ về một index local duy nhất. Không bước nào cần mạng. Điều đó đáng giá khi thứ bạn đang lưu là screenshot số cột ở ba tầng dưới vùng phủ sóng.

Spaces là tập hợp, file là bản duy nhất

Sắp xếp diễn ra sau khi capture. Item sống trong Spaces: những bộ sưu tập user tự đặt tên như "Log QA hầm xe", "Paper DSP" hay "Link nấu ăn". Một item thuộc nhiều Spaces cùng lúc mà file gốc không bị nhân bản. Bên trong, tư cách thành viên trong một Space chỉ là một dòng ở join table; blob tồn tại đúng một lần trên đĩa, có đếm tham chiếu, nên gỡ item khỏi Space cuối cùng mới là lần xóa duy nhất động tới storage. Nghe như chuyện vặt về database, cho tới khi bạn thấy ai đó file một tờ datasheet vào cả "Paper DSP" lẫn Space của dự án rồi mặc nhiên chờ ghi chú sửa ở chỗ này hiện ra ở chỗ kia. Nó hiện thật, vì chỉ có một item. Tìm keyword chạy trên title, tag user thêm và phần text đã trích; có prefix matching nên gõ "barom" là tài liệu áp kế đã hiện trước khi gõ xong.

Tìm theo nghĩa, vẫn offline

Keyword fail theo một kiểu rất cụ thể: nhớ khái niệm nhưng quên mất từ. Cho tình huống đó, Stashio có tùy chọn tính embedding vector cho từng item ngay trên máy; index vector cũng nằm local. Query kiểu "cái gì đó về áp suất thủy tĩnh trong hầm đỗ xe" vẫn match được một bản draft blog đã lưu, dù chữ "thủy tĩnh" chưa từng xuất hiện trong tag. Model là một sentence encoder nhỏ cho vector 384 chiều; độ tương đồng là cosine thường, cos θ = (a·b)/(|a||b|); ở quy mô thư viện cá nhân thì chẳng cần gì khôn hơn. Thư viện 2.400 item của mình chứa trọn index vector trong khoảng 3,7 MB (2.400 × 384 float × 4 byte), quét brute-force toàn bộ mất chưa tới 2 ms trên iPhone 12. Team có bench thử một cấu trúc approximate nearest neighbor, rồi bỏ: ở quy mô này ANN tiết kiệm vài microsecond nhưng bắt rebuild index mỗi lần sửa.

Xếp hạng trộn cả hai tín hiệu:

// mỗi query, chạy local hết
kw_scores  = bm25(query, titles + tags + bodies)
q_vec      = embed(query)            // 384 chiều, on-device
sem_scores = {}
for item in library:                 // 2.400 vòng, dưới 2 ms
    sem_scores[item] = cosine(q_vec, item.vec)
final(item) = 0.6 * norm(kw_scores[item])
            + 0.4 * norm(sem_scores[item])
return top_k(final, 20)

Tỷ lệ 0,6/0,4 ra từ bảng chấm relevance của ktuyen chứ không phải từ lý thuyết. Cô chấm 60 query thử trên hai thái cực và bản trộn; semantic thuần cứ xếp tờ hóa đơn mua ủng chống nước lên trên một paper thủy động lực học thứ thiệt.

Vì sao app này được đụng cloud

Bookmark không cần latency 10 ms. Không ai mất ngủ vì share extension trễ deadline buffer audio; thế giới đó thuộc về audio thread, nơi 10,7 ms là bức tường cứng. Thứ một công cụ save-later thật sự cần là sync đa thiết bị. Lưu trên điện thoại, tìm thấy trên tablet; đó mới là tính năng người ta chịu trả tiền. Nên atuan dựng một backend nhỏ, vẫn theo bản năng privacy của cả studio. Cache local luôn đọc được khi offline. Tài khoản là tùy chọn. Với sync embedding, server chỉ giữ blob đã mã hóa chứ không giữ nội dung plaintext. Capture và search chạy trọn vẹn khi chưa từng đăng nhập, vì index được build trên máy trước tiên và vẫn tìm được ở chế độ máy bay. Mấy app cảm biến chạy mọi thứ local vì vật lý bắt buộc; Stashio chừa một làn cloud vì workflow của user bắt buộc. Cùng một quyết định, hai đầu ngược nhau — bài on-device-first đi hết cả phổ đó.

Bài stress test 2.400 link

Trước khi có Stashio, Apple Notes của mình chứa chừng 2.400 link không phân loại, gom trong bốn năm test hầm xe, đọc paper DSP và lang thang internet. Đống đó thành corpus hạt giống. Sau ba tháng dùng hằng ngày, khoảng 94% lượt truy hồi của mình thành công dưới 10 s, bằng keyword hoặc bằng nghĩa. Con số lấy từ log thật: app ghi lại, local thôi, mỗi phiên tìm kết thúc bằng một lần mở hay một lần bỏ cuộc. 6% còn lại fail một cách lương thiện, phần lớn là item có text trích ra rỗng, kiểu screenshot chụp lại screenshot. atuan dùng Stashio chứa mấy snippet tài liệu API cần lúc làm backend. ktuyen đính screenshot test case và log regression vào Spaces trước mỗi chu kỳ release, nghĩa là lịch sử QA của mọi bản phát hành uranashel giờ nằm trong chính một app đang bị QA. Không ai lên kế hoạch cho chuyện đó.

Dấu vân tay chung của studio

Stashio không phải dụng cụ vật lý; trong màn settings của nó không giấu cái Kalman filter nào. Nhưng nhìn vẫn ra người nhà. Vẫn ngôn ngữ UI đơn sắc ấy, vẫn Nunito trên từng label, vẫn bo góc 12 px, vẫn bộ string song ngữ EN/VI được ktuyen soát từng dòng. Hệ thiết kế được ghi lại trong bài monochrome; nó chuyển sang một app bookmark mà không phải sửa gì — team coi đó là bằng chứng đây là một hệ thống chứ không phải trùng hợp. Trang app ở đây. Stashio giải một lớp bài toán khác hẳn Wheria. Chính khác biệt đó là lý do nó đứng cạnh Wheria trong danh mục thay vì thay thế: mấy app cảm biến chứng minh điện thoại làm được dụng cụ đo, còn Stashio chứng minh vẫn ba con người đó biết tôn trọng một bộ ràng buộc hoàn toàn khác khi bài toán yêu cầu.