Archival & Recovery
SECTION 26A — FULL-AY ARCHIVAL & RECOVERY
26A.1 Overview & Toggle
In v4, only images were archived at the academic-year boundary (§26.4). In v5, the Organisation owner (“Teacher”) can opt into a Full-AY Archive that bundles all data produced during the previous academic year — JSON content, generated PDFs, generated PPTX files, all images, Q&A nodes, revision history — into a single ZIP archive. The archive destination depends on the §25.0 Drive sync toggle: if Drive is enabled, the ZIP is uploaded to the Teacher’s Google Drive; otherwise it lives in the backend object store and the Teacher downloads it via an in-app signed link. A reciprocal recovery flow lets the Teacher restore an archived AY by uploading the same ZIP back into the app — same flow regardless of where the ZIP was originally stored. Toggle:organisations.full_ay_archive_enabled (boolean, default false). Configurable by the Teacher (Organisation owner) only — Sub-members, Editors, and Super Admins cannot toggle this. Surfaced in Teacher Dashboard → Organisation Settings → Data → “Full-AY Archive”.
Relationship to §26.4 image-only archive:
- When
full_ay_archive_enabled = true: the image-only archive cron from §26.4 is suppressed; the full-AY cron (§26A.2) handles images as part of the full ZIP. Single ZIP, single source of truth per AY. - When
full_ay_archive_enabled = false: the §26.4 image-only flow runs as documented in v4. No full-AY archive is produced.
26A.2 Archive Cron Job
- Schedule: Fires at the end of the month following the Teacher’s configured academic year end (same grace period as §26.4 — one calendar month after AY end).
- Independence: Each Organisation’s cron evaluates independently against its own AY end + toggle state.
- Concurrency: The cron job for a given Org is mutually exclusive with that Org’s §26.4 image-only cron — only one runs per AY boundary, dispatch decided by the toggle state at trigger time.
- Snapshot all relevant Neon DB rows for the concluding AY for this Org:
documents,document_versions,pdf_outputs,images,revision_screenshots,qa_items,pipeline_events(scoped to the AY). - For each document, write
content.json(the latestdocuments.content_json) and allversions/*.json(fromdocument_versions). - For each PDF in
pdf_outputs, fetch the file and write topdfs/. - For each PPTX exported within the AY whose binary was retained (note: PPTX is client-generated in v5; only PPTX explicitly saved to Drive is collected here), write to
pptx/. - Fetch every Cloudinary asset for the AY (originals, not crop-transformed) and write to
images/. Image filenames follow §26.3 convention. - Compute SHA-256 of every file written.
- Build
manifest.json(see §26A.3). - Sign the manifest with the Org-scoped HMAC key.
- ZIP the entire tree (DEFLATE level 9) as
[Organisation Name]-AY-[YYYY-YYYY].zip. - Destination (depends on Drive sync state, §25.0):
- If
organisations.drive_sync_enabled = true→ upload to Teacher’s Drive at[Organisation]/[AY]/ARCHIVE/FULL-AY-[YYYY-YYYY].zip. Teacher downloads from Drive. - If
organisations.drive_sync_enabled = false→ write to the backend object store atorg-archives/<org_id>/<academic_year>.zip. Teacher downloads via a signed in-app link surfaced in Organisation Settings → Data → Archives. The same HMAC manifest, recovery flow (§26A.4), and conflict rules (§26A.5) apply identically — recovery accepts an upload from local disk regardless of where the ZIP came from.
- If
- After Drive upload is confirmed: purge Cloudinary assets for the AY (same as §26.4 step 6), mark
images.is_archived = true, setdocuments.is_archived = truefor AY documents, setorganisations.full_ay_archived_years += [YYYY]. - Email Teacher: “Your AY [YYYY-YYYY] data has been archived. Download or recover via Organisation Settings → Data → Archives.”
26A.3 Archive ZIP Structure & Signed Manifest
manifest.json shape:
- Backend holds an Org-scoped HMAC secret (
organisations.archive_hmac_secret, generated on Org creation, encrypted at rest). manifest.sig = HMAC-SHA256(manifest.json bytes, org's secret).- Signature is verified by the same secret on recovery. The secret never leaves the backend; the ZIP carries the signature, not the secret.
26A.4 Recovery Flow
Trigger: Teacher (Organisation owner only) → Organisation Settings → Data → “Recover AY” → uploads aFULL-AY-[YYYY-YYYY].zip from local disk or Google Drive picker.
Backend recovery sequence:
- Receive ZIP, extract to a sandboxed temp directory.
- Validate structure: required top-level dirs and
manifest.json+manifest.sigmust exist. If missing → reject with error: “Archive file is malformed.” - Parse
manifest.json: readorg_idandacademic_year. - Authorisation check:
manifest.org_idmust equal the current Teacher’sorg_id. If not → reject: “Archive belongs to a different organisation.” (Logged toaudit_logs.) - Verify signature: recompute
HMAC-SHA256(manifest.json bytes, current_org.archive_hmac_secret)and compare tomanifest.sig. If mismatch → reject: “Archive signature is invalid — the file may be corrupted or tampered with.” (Logged toaudit_logswith HMAC-failure event.) - Verify file hashes: each entry in
manifest.files[]is recomputed and compared. Any mismatch → reject with the specific failing path. - Conflict check (§26A.5): refuse if any data already exists for
(org_id, academic_year). - Image re-upload (§26A.6): re-upload all
images/*to Cloudinary; collect new URLs keyed byatomic_uid. - Restore DB rows: insert documents, versions, pdf_outputs, qa_items, revision_screenshots, images, pipeline_events. For every image URL reference in
content_json(thumbnailurl, Brief Lexical tree image-nodesrc, derivedreference-images[*].url), look up the new Cloudinary URL byatomic_uidand rewrite. - Re-upload PDFs/PPTX to Drive at their original paths (overwrite-safe because §26A.5 guarantees no AY data exists).
- Mark archive consumed:
organisations.full_ay_archived_years -= [YYYY]; flag the ZIP in Drive as “recovered” (rename suffix.recovered.zip). - Notify Teacher: “AY [YYYY-YYYY] recovered successfully. [N] documents, [M] images restored.”
26A.5 Conflict Handling
Rule: Recovery is refused if any data already exists in the target Org for the target AY. Concretely, the backend runs:count > 0, the recovery is aborted with: “AY [YYYY-YYYY] already contains [N] documents. Recovery would overwrite or duplicate existing data. To proceed, the existing AY data must be deleted first (Organisation Settings → Data → Delete AY).” The Teacher is offered a CTA to navigate to the destructive-delete flow (separate confirmation, separate audit log entry).
Rationale: Full-AY recovery is a high-impact operation. Per-record merge dialogs are out of scope for v5 (overengineering). The lean rule — refuse on conflict, force explicit user-driven cleanup — yields zero ambiguity and zero partial-state bugs.
26A.6 Image Re-Upload on Recovery
Cloudinary deletes archived images per §26.4 step 6 (or as part of §26A.2 step 11). On recovery the URLs incontent_json are dead. Re-uploading is required.
Procedure:
- For each file in
images/, the backend uploads to Cloudinary withf_webp, preserving the file content bit-for-bit (no re-encoding). - Inject
atomic_uidinto Cloudinary MIME metadata (§11A.5). - Insert a new
imagesrow with the newcloudinary_urlandcloudinary_public_id, reusing the originalatomic_uid(the UID is the stable identity, not the URL). - Build an in-memory map
atomic_uid → new_cloudinary_urlfrom the manifest’satomic_uid_index. - For each document’s restored
content_json, walk:Newsletter.categories[*].News-Items[*].images.thumbnail.url— rewrite byatomic_uid.Newsletter.categories[*].News-Items[*].Brief.root.children[]Lexical tree — rewrite every image node’ssrcby itsatomic_uid.- Derived
images.reference-images[*].url— rebuilt from the Brief walk.
- Persist the rewritten
content_jsontodocuments.content_jsonand to all correspondingdocument_versions.content_json.
26A.7 Retention of Archive ZIPs in Drive
- Archive ZIPs in Drive are never auto-deleted by Notesglider. They remain in the Teacher’s Drive at
[Organisation]/[AY]/ARCHIVE/FULL-AY-[YYYY-YYYY].zipindefinitely or until the Teacher manually deletes them in Drive. - If the Teacher deletes the ZIP from Drive, recovery becomes impossible. No app-side warning is enforced for Drive-side deletions; the Teacher owns their Drive.
- Drive storage usage warning surfaces during the §26A.2 upload step if Drive is approaching its quota (reuses §28.4 storage-limit handling).