PPM

@pixygon/savedata

Unified save data system for all Pixygon applications — auto-sync, conflict detection, ownership guard

Install

# .npmrc
@pixygon:registry=https://ppm.pixygon.io/

npm install @pixygon/savedata@1.1.3

Versions

VersionPublishedIntegrity
1.1.3 latest2026-09-30sha512-8IatpBfhsd9D/3m…
1.1.2 2026-05-01sha512-XcvE8+ralQGVI8k…
1.1.1 2026-04-30sha512-q4ctPTBrG1gzfRC…
1.1.0 2026-04-25sha512-lHTR6JVx5rcE3/E…
1.0.0 2026-04-25sha512-va8N+rgpHU6zgUs…

@pixygon/savedata

Unified, server-synced save data for Pixygon apps — auto-save, dirty tracking, revision-based conflict detection, and an ownership guard. 6 repos depend on it (Tastebud, Kartograf, CoreFeed, SoloBattle, pixygon.io, pixiel.ai), and several other pearls (@pixygon/social, /quests, /inventory, /anima) store their state through the same endpoints.

Backed by /v1/savedata/:projectId/:userId on the Pixygon API. The Unity counterpart is com.pixygon.saving (related, deliberately separate).


The mental model

A save is projectId → userId → slot → { section: {...}, section: {...} }.


Retrofitting it into an existing app (≈15 minutes)

1. Install

npm i @pixygon/savedata

Requires @pixygon/auth (or any token source) — the config needs a live token and user id.

2. Mount the provider(s) — inside AuthProvider

// src/providers/SaveDataProvider.tsx — the estate pattern (Tastebud/Kartograf)
import { SaveProvider } from '@pixygon/savedata'
import { useToken, useAuth } from '@pixygon/auth'
import type { SaveConfig } from '@pixygon/savedata'

const API_URL = import.meta.env.VITE_API_URL || 'https://api.pixygon.com/v1'

function GlobalSaveProvider({ children }: { children: ReactNode }) {
  const { getAccessToken } = useToken()
  const { user } = useAuth()
  const config: SaveConfig = {
    baseUrl: API_URL,
    projectId: '65481fec93ee5f866ade917f',   // shared cross-app project
    getToken: getAccessToken,
    getUserId: () => (user as any)?._id ?? null,
    autoSaveInterval: 15_000,
  }
  return <SaveProvider config={config}>{children}</SaveProvider>
}

function AppSaveProvider({ children }: { children: ReactNode }) {
  /* same, with projectId = this repo's .pixygon.json ObjectId, 10s interval */
}

export function SaveDataProvider({ children }: { children: ReactNode }) {
  return <GlobalSaveProvider><AppSaveProvider>{children}</AppSaveProvider></GlobalSaveProvider>
}

Providers nest; each gets its own React context keyed by projectId.

All four of baseUrl, projectId, getToken, getUserId are required. With no getUserId the provider never loads and never saves — silently.

3. Read and write

import { useSavedata, useSavedataSection } from '@pixygon/savedata'

// whole save for one namespace
const { data, isLoaded, syncStatus, revision, updateSection, mergeSection, forceSave, forceLoad } =
  useSavedata<MySave>('69ee9b3f7a116f8ba90d0890')

updateSection('lists', { items })          // replace a section
mergeSection('prefs', { theme: 'dark' })   // shallow-merge into a section

// one section, with a default
const { data: skills, update, merge } =
  useSavedataSection('skills', { skillXp: {}, totalXp: 0 }, '65481fec93ee5f866ade917f')

⚠ useSavedata('<id>') with an id that has no mounted provider THROWS (no SaveProvider for projectId "…"). With multiple providers mounted, always pass the id explicitly — the no-arg form resolves to the last mounted provider, which is order-dependent and easy to get wrong.

4. Verify it worked

node node_modules/@pixygon/savedata/verify.mjs
# or:
pearl verify savedata

Checks: installed, <SaveProvider> mounted, all four SaveConfig fields present, a non-placeholder projectId (flagging slugs and unexpected ObjectIds), a real baseUrl, and that every useSavedata(projectId) call targets a mounted provider. Exit 0 pass, 1 fail.

What verify cannot prove: that a save round-trips. Do it by hand once: sign in → change something → wait for syncStatus: 'synced' → hard-reload → confirm it survived. Watch the network tab for 409 (revision conflict) and 403 (ownership guard).


API

ExportUse
<SaveProvider config>the mount (one per save namespace)
useSavedata<T>(projectId?)data, isLoaded, syncStatus, revision, lastSyncedAt, error, updateSection, mergeSection, forceSave, forceLoad
useSavedataSection<T>(key, default, projectId?)data, isLoaded, syncStatus, update, merge
useSaveContext(projectId?)raw context (throws if not mounted)
loadFromServer / mergeToServer / updateSectionOnServerimperative, outside React

SaveConfig

{
  baseUrl: string                  // https://api.pixygon.com/v1
  projectId: string                // the save namespace — permanent
  getToken: () => string | null
  getUserId: () => string | null
  slot?: number                    // default 0
  autoSaveInterval?: number        // default 10_000 ms (0 = off)
  saveOnVisibilityHidden?: boolean // default true
  saveOnBeforeUnload?: boolean     // default true
  debug?: boolean
}

syncStatus: idle | loading | synced | saving | conflict | error.

Safety features (and what they mean when they fire)

Gotchas

Publishing

npm run build && npm publish --access public

All packages · packument JSON