CanPUFF canpuff.org

Open standard · v1.0-rc · October 2026 revision

A file format for personal cannabis records.

CanPUFF documents the data an individual creates when they track what they consume, what they have, where they got it, and how it affected them. It is a format for people, not businesses — and it is designed to outlive any application, including the ones its authors write.

Think “RSS for personal cannabis data”: plain files you own, readable in any text editor, portable between apps, with no account, no API key, and no company that can take them away.
Round-tripped
1,752 records
Attachments
239 photos
Implementations
2
Fields lost
0

The gap

Every part of the supply chain has a data standard. The person at the end of it has none.

Seed-to-sale systems, dispensary point-of-sale, and regulatory traceability are all standardised and interoperable. Meanwhile every consumer tracking app keeps its records in an undocumented internal database — so the data dies with the app, and years of personal history go with it.

CanPUFF is the missing half: a documented, implementable format for the consumer's own records, published under CC0, with conformance fixtures so a second implementation can prove it agrees with the first.

The documents

What is specified, and how firm it is.

canpuff-v1.md

The vault layout, the journal (JSONL events), the catalog (Markdown + YAML frontmatter cards), attachments, identifiers, units, import merging, and the extension mechanism. Scoped to inhaled cannabis, recorded by weight.

Frozen · 12 sections

canpuff-sealed-v1.md

End-to-end encryption with age v1, a key hierarchy rooted in a BIP39 mnemonic, and a synchronisation protocol that works against a dumb file server which never sees plaintext.

Under active development

Edibles profile

draft r1
canpuff-edibles-v1.md

Edibles, kept out of the core on purpose: pieces instead of grams, milligrams per piece instead of mg/g, servings instead of weighed doses. It adds fields to core objects; an application that ignores it simply carries them through.

Companion profile · no implementation yet

mapping-pufftab-ios.md

Field by field, lossless, both directions: how the first implementation's 13-entity SwiftData model becomes CanPUFF and comes back unchanged.

Conformance contract

JSON Schemas

1.0-rc
schemas/v1/<type>.json

JSON Schema 2020-12 for every object type, served at the canonical $id URLs. Normative for the JSON form.

12 schemas · 3 profile overlays

Examples

1.0-rc
examples/

A complete example plain vault, plus one standalone JSON object per type that validates against the schemas.

Copy-paste starting points

fixtures/

Edge-heavy valid vaults and deliberately broken ones, each with the reference validator's expected verdict, plus a runner. This is what makes a third-party implementation testable.

Distributed via git, not the site

tools/

validate-vault.py for conformance and diff-vaults.py for semantic equivalence of two vaults. Standalone, no dependencies; both accept a directory or a .canpuff.zip.

Round-trip verification

At a glance

The whole format is a directory you can read without us.

Two tiers: a private journal of events, and a shareable catalog of documents. Binary files are content-addressed by SHA-256. Nothing else.

The plain vault · Core §3
<vault>/
  manifest.json                    REQUIRED  vault identity & format version
  journal/
    <YYYY>/<MM>.jsonl              journal events, one JSON object per line
  catalog/
    supplies/<id>.md               shops · chains · brands · producers
    methods/<id>.md                tax-rates · terpenes
  attachments/
    <hh>/<sha256>.<ext>            content-addressed binary files
  apps/
    <app-id>/...                   application-private data, carried through intact

Readers must tolerate unknown files and directories, so the format can grow without breaking anything.

A journal event · journal/2026/07.jsonl
{
  "type": "consumption",
  "version": 1,
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "at": "2026-07-09T16:20:00-07:00",
  "supply": "9f3c1a2e-7b41-4c8e-9d2a-51f08c77b3aa",
  "amount": { "grams": 0.35 },
  "shared": { "people": 2, "gift": false },
  "effects": { "rating": 7 },
  "notes": "Paired with a movie."
}

Pretty-printed here; on disk it is one line. Private by default — journal data never leaves a trusted device unencrypted.

A catalog card · catalog/supplies/9f3c….md
---
type: supply
version: 1
id: 9f3c1a2e-7b41-4c8e-9d2a-51f08c77b3aa
name: Pink Kush
variety: indica
thc: 22.5
terpenes:
  - { name: Myrcene, percentage: 0.8 }
grams: 2.1
cost: 29.99
currency: CAD
---

Dense buds, gassy. Better in the bong than rolled.
Second time buying — first batch was drier.

Opens in any text editor, and natively in Obsidian. Contains nothing about the user's behaviour, so it is safe to share.

Design principles

Seven decisions that explain most of the spec.

  1. File over app

    The format is the product. Applications — including PuffTab — are replaceable CRUD interfaces over it.

  2. Journal / catalog split

    Events are private by default and never leave the user's control unencrypted. Product, shop and brand records contain nothing personal and are inherently shareable.

  3. Human-legible where a human might look

    Catalog cards are Markdown with YAML frontmatter. The journal is JSONL — still greppable, built for machines.

  4. Machine-exact where machines interoperate

    The canonical data model is JSON, specified by JSON Schema. Interchange, QR payloads and validation always use the JSON form.

  5. Private by architecture, not policy

    Consumption records are health data and, in some jurisdictions, self-incriminating. The format treats that as a design input, not a disclaimer.

  6. Extensible without forking

    Unknown fields are ignored, exactly one blessed extension mechanism exists, and every object carries type and version.

  7. Boring on purpose

    UUIDs, RFC 3339, SHA-256, JSON Schema, age encryption. No invented primitives, no clever tricks to regret later.

Status & governance

A release candidate, revised for richer records.

The core format is a v1.0 release candidate. The September 23 revision adds named cannabinoids in mg/g, cultivar, lineage, tip, and receipt number within v1. Earlier vaults remain readable; older implementations may not safely edit the new fields or cannabinoid catalog. This revision supersedes the earlier freeze for these changes; future revisions follow the versioning rules in Core §10. The sealed profile remains a draft, and v1.0 final follows its validation.

The October revision states the core's scope: cannabis used by inhalation, recorded by weight. Edibles are deliberately not part of it; they have a companion profile (a draft) that adds to the same vault without changing the core.

Spec text and schemas are published under CC0-1.0, with an OWFa 1.0 patent non-assertion covering implementations. Feedback, implementations and proposals are all welcome — the extension mechanism is the intended first stop for a new need, and fields that prove themselves in ext are candidates for the next minor version.