TEASEDocs
ProductsClubConcepts

Vault & content concepts

The mental model behind the content vault — what the index actually is, how OnlyFans writes are staged, and the shared truths content endpoints all reuse.

The vault is TEASE's read model over your OnlyFans content library, plus a thin, carefully gated write path back onto it. Everything under /api/admin/vault and /api/admin/content builds on the ideas below — read this first if a how-to page assumes one of them.

The vault index is a mirror, not an upload target

VaultItem rows in our database are a searchable copy of what's already on OnlyFans. You never upload media to build the index — you trigger a scan, and the scan reads your already-captured feed (or OFAPI) and writes rows locally. Search, folders, drops, performance, gaps, dormant, and recommendations are all reads over this mirror — none of them touch OnlyFans directly.

This is why search is instant and free: GET /vault/search is a LIKE query over a local table, not a live OnlyFans call.

Deep scan is an engine-only full crawl

The normal scan (POST /vault/scan) re-reads whatever the engine has already captured. A deep scan (deep=1) is different: it writes a deep_scan.request control file into your own feed directory so the engine's next poll crawls your entire vault — every cover, every item — in one slow pass. That crawl only exists on the gateway engine; an account running in OFAPI mode has no engine to drive it, so deep=1 there is a 400, not a slower scan.

A drop is a folder read as a sellable bundle

A drop groups one folder's items by role — video, photoset, bts (behind-the-scenes), other — and hands back one ordered bundle_media_ids array: the whole folder, ready to drop into a single PPV send. Role is classified from the media type plus a BTS keyword match against the title/tags (English and Russian variants). is_drop is not a mood flag — it's computed: true only when the folder has at least one video and at least one photoset or BTS item.

Vault management has two levels, and they can never blur

  • Level 1 — local organization. Favorite, block, note, and named collections write only to our database. No engine involved, no readiness gate, no queue — click and it's saved.
  • Level 2 — OnlyFans folder operations. Creating, renaming, or deleting a folder, and adding or removing media in one, are real writes to OnlyFans. They go through the engine as an asynchronous job, and they live in a separate router module (vault_ops.py, not vault_search.py) specifically so the two worlds can't be confused in the code.

Uploads never send bytes through our backend

Adding a new file to the vault is a three-party handoff: our engine asks OnlyFans for an upload address, your browser PUTs the file bytes straight to that address, and the engine closes the upload once the bytes land. Not one byte of your media passes through the TEASE backend — it only ever sees a name, a size, and a content type going in, and ETags coming back. One consequence: there's no resume. If the upload is interrupted, you start it again; the engine cleans up the abandoned multipart parts.

One shared truth for "did the fan buy this"

core.ppv.signal exports exactly three functions — is_purchased, is_sent_unbought, is_free_send — and every place in Club that needs to know whether a PPV was bought reuses them: the content library, the payment-content resolver, and the sales poller. A purchase is never inferred by one team from canPurchase == False alone, because OnlyFans also clears that flag on a revoked or expired send — is_purchased additionally requires the fan to have opened the message.

There is no field on an OnlyFans transaction that names which vault media it bought. For a content-less PPV, TEASE resolves the answer the same way the OnlyFans app's own UI does: it opens the fan's chat live, finds the purchased message nearest the payment in time and price, and reads its media off that message. The result is then cached into the content_sale table so the next lookup for the same purchase is instant and never re-reads the chat.

Tags carry provenance, and that provenance is enforced

A ContentTag row always carries a source: manual (a person typed it), or ai (either the vision tagger over thumbnails, or the text auto-tagger — both write the same vocabulary so they group together). The API enforces the distinction: deleting a tag only ever removes a manual one. Asking to delete an AI/vision tag returns an honest 404, not a silent no-op or a delete of the wrong row.

A pending nonce, not a promise, is what survives a crash

Every OnlyFans-writing endpoint in the vault — folder ops, uploads — answers {"status": "pending", "nonce": "..."} immediately, never {"status": "done"}. The real state lives in the engine_job journal and is only updated once a background reconciler confirms what actually happened. This isn't about the engine being slow (the underlying OnlyFans call takes seconds) — it's about surviving an engine crash, a pod restart, or a dropped connection mid-request: a synchronous wait inside the HTTP call doesn't survive any of those, and a lost wait invites a retry that would create a second folder or upload on OnlyFans. Where a retry path exists (vault folder ops), it re-submits under the same nonce for exactly this reason — see async engine write gotchas.

What's next

Start with finding content to send, or jump to indexing the vault if nothing is searchable yet.

On this page