@freeappstore/sdk

中文文档
0.17.1发布于 2026/11/14MITTypeScriptES Module构建来源已验证近 30 天下载 1.5万

Browser SDK for FreeAppStore — auth, KV, collections, counters, rooms, proxy, encrypted user keys, roles, friends, email, push reminders, device-local persistence, validated backup and file export, React hooks, and UI components.

原始使用文档
文档目录20 节
  1. Installation
  2. Usage
  3. Modules
  4. Auth
  5. Data access contract
  6. Per-user KV
  7. Shared counters
  8. Realtime rooms
  9. Collections (document database)
  10. Free APIs & libraries (no proxy needed)
  11. Secret-injecting proxy
  12. User API Key Vault
  13. Push reminders ( fas.push )
  14. React Hooks
  15. App shell
  16. UI Components
  17. For AI Agents
  18. License
  19. Tailwind styling (SDK 0.16+)
  20. Temporary photos ( fas.photos )

@freeappstore/sdk

Browser SDK for free apps published on freeappstore.online.

PWA lifecycle APIs live at @freeappstore/sdk/pwa, with Vite options at @freeappstore/sdk/pwa-config. See safe updates and offline capabilities.

Device-local preferences, IndexedDB, and large-file metadata live at @freeappstore/sdk/persist. That data is not the signed-in account. Account keys remain fas.kv. A failed save returns a status instead of throwing a success path, and corrupt values are left in place. See device-local persistence.

Validated backup, import, and browser file export live at @freeappstore/sdk/backup. This subpath requires @freeappstore/sdk >=0.16.21. The app owns the records and the merge policy. Import is all-or-nothing and does not delete data when the capability is installed or removed. See backup and file export.

Installation

npm i @freeappstore/sdk

Usage

import { initApp } from '@freeappstore/sdk';

const fas = initApp({ appId: 'my-app' });

// Capture the OAuth callback if we're returning from sign-in. Call this once
// at app start, before any UI that depends on auth state.
await fas.auth.init();

// React to sign-in / sign-out.
fas.auth.onChange((user) => {
  console.log(user ? `Hello @${user.login}` : 'signed out');
});

// Trigger sign-in (redirects to GitHub).
document.querySelector('#signin')?.addEventListener('click', () => {
  fas.auth.signIn();
});

Modules

Auth

GitHub OAuth via redirect. Session persists in localStorage.

fas.auth.user;             // User | null
fas.auth.token;            // string | null
fas.auth.signIn();         // redirects to GitHub
fas.auth.signOut();        // signs out of this app
fas.auth.onChange(cb);     // fires immediately + on change, returns Unsubscribe
await fas.auth.init();     // capture callback, must be called once

Data access contract

ModelServiceGuarantee
Device-local@freeappstore/sdk/persistThis browser profile and origin only; not account sync.
Private per-accountfas.kvServer-side (appId, userId) isolation; authentication required.
Shared documentsfas.collectionsOwner-only until the app owner shares the collection (fas db share); then public reads, owner-only writes. A shared collection is public even when a query filters by owner.
Room-restrictedBackend data-room HTTP APIAuthenticated endpoints enforce room membership. They are separate from public collections and realtime room broadcasts; the SDK does not yet provide a wrapper. See the data-room API.

A collection is 'private' (owner-only) by default. fas.collections.collection(name, { visibility: 'public-read' }) declares in code that the collection is shared; the server enforces what the app owner set with fas db share. Any other visibility is rejected by the SDK type and runtime.

Per-user KV

Private per-account cloud storage scoped to (appId, userId) server-side. Use it for data that follows a signed-in user across devices. Requests require the account; do not gate unrelated public screens behind sign-in.

await fas.kv.set('theme', { color: 'plum' });
const theme = await fas.kv.get<{ color: string }>('theme');
await fas.kv.delete('theme');

const allKeys = await fas.kv.list();
const noteKeys = await fas.kv.list({ prefix: 'note:' });
const notes = await fas.kv.getMany<Note>(noteKeys);

// All methods accept { signal } for AbortController cleanup
const ctrl = new AbortController();
await fas.kv.get('key', { signal: ctrl.signal });

Limits (server-enforced): max 1MB per user, max 100 keys per user, max 64KB per value.

Shared counters

App-wide atomic counters — not user-scoped. Anyone can read; authenticated users can increment. Decrementing is reserved for the app owner and users with a privileged app role (anything other than member / viewer). Use for vote tallies, view counts, leaderboards.

// Read (no auth required)
const all = await fas.counters.list();           // { likes: 5, views: 100 }
const views = await fas.counters.get('views');   // 100

// Increment (auth required)
const newVal = await fas.counters.increment('likes');      // +1, returns new value
const newVal2 = await fas.counters.increment('score', 10); // +10
await fas.counters.increment('lives', -1);                 // decrement — owner / privileged role only

Limits: max 1000 counters per app, increment range -1000 to +1000 per call.

Realtime rooms

Durable-Object-backed WebSocket fan-out. Ephemeral — messages are not persisted. Sized for cursor presence, light collab, and Slither-style multiplayer (low state, high frequency).

const room = fas.rooms.join('lobby');

room.onPeers((peers) => console.log('peers:', peers));
room.onMessage<{ text: string }>((msg) => {
  console.log(msg.from, msg.data.text);
});

room.send({ text: 'hello' });
// later:
room.close();

Rooms provide ephemeral realtime broadcast, not durable storage. Rooms are open to any signed-in user by default. fas.rooms.join('staff', { private: true }) marks a room private: from then on only users who hold an app role for this app (the app's owner, or anyone the owner granted a role with the roles API; the member role every user gets automatically does not count) can join it, and only such a user can make a room private. Everyone else gets room.denied === true (state error, no retry). { private: false } opens a private room again. Private rooms gate who can join; messages are still ephemeral broadcast, not stored. Do not claim confidentiality beyond who can join. Limits (server-enforced): 32 peers per room, 100 msgs/sec per peer, 4KB per message, 24h idle eviction, 64 active rooms per app.

Collections (document database)

Document store for queryable data. Documents are private to their owner by default; fas db share <collection> lets everyone read a collection (authenticated owners still write only their own). owner can only be your own id, and _owner is hidden from other users. No SQL required — just JSON in, JSON out.

const posts = fas.collections.collection('posts', { visibility: 'public-read' });

// Create (auth required, you become the owner)
const post = await posts.create({ title: 'Hello', body: '...' });

// Query (your own documents, or everyone's once shared)
const { documents, total } = await posts.query({
  limit: 20,
  orderBy: 'created_at',
  order: 'desc',
  owner: userId,  // optional: only ever your own id
});

// Get single document
const doc = await posts.get('doc-id');

// Update (owner only)
await posts.update('doc-id', { title: 'Updated' });

// Delete (owner only)
await posts.delete('doc-id');

Limits: 10,000 documents per collection, 64KB per document.

Free APIs & libraries (no proxy needed)

Many useful tools require no API key. Use them directly: Leaflet (maps), Recharts (charts), Tiptap (rich text), date-fns, react-markdown, qrcode.react, dnd-kit (drag & drop), Framer Motion, Lucide React (icons), React Hook Form, Zustand. Free APIs: Open-Meteo (weather), Nominatim (geocoding), OSRM (routing), REST Countries, and more. See SKILLS.md for the full list.

Secret-injecting proxy

For APIs that DO need a key, the proxy encrypts and injects it server-side.

const res = await fas.proxy.fetch('api.openweathermap.org/data/2.5/weather?q=London');
const data = await res.json();

User API Key Vault

Users store their own API keys on the platform (encrypted AES-256-GCM). Apps never see plaintext keys.

// Check if user has a key configured
const hasKey = await fas.keys.has('openai');

// Redirect to platform key management page
fas.keys.manage('openai');

// Check all configured providers
const keys = await fas.keys.status();

Supported providers: OpenAI, Anthropic, Google AI, OpenRouter, Replicate, Stability AI, ElevenLabs.

Push reminders (fas.push)

Scheduled Web Push notifications for the signed-in user. The platform sends a push to each of the user's devices for your app when a reminder comes due (checked every 15 minutes).

const { supported, reason } = fas.push.isSupported();
// reason 'requires-home-screen': on iPhone/iPad, add the app to the Home Screen first

// From a button tap (browsers block permission prompts outside a user gesture):
await fas.push.subscribe(); // asks permission, registers this device

await fas.push.schedule('period-due', { at: new Date('2026-11-02T09:00') });
await fas.push.schedule('log', { daily: '21:00' }, { quietHours: { start: '22:00', end: '07:00' } });
await fas.push.schedule('refill', { everyDays: 28, at: '09:00' }, { tz: 'Europe/London' });

const reminders = await fas.push.list(); // [{ reminderId, when, tz, quietHours, nextAt }]
await fas.push.cancel('log');
await fas.push.unsubscribe(); // this device only

Errors: PushNotSignedInError, PushNotSupportedError (with reason), PushPermissionDeniedError, PushNotConfiguredError.

Pushes carry no payload: your service worker shows the notification text, so nothing about the reminder passes through the browser's push service. With vite-plugin-pwa's injectManifest strategy, add this to your service worker:

import { registerFasPushHandler } from '@freeappstore/sdk/push-sw';
registerFasPushHandler({ title: 'Cycle', body: 'Time to check in.', onClick: () => '/' });

Keep the text generic for anything sensitive; it can show on a lock screen. Limits: 10 devices and 20 reminders per user per app.

React Hooks

Import from @freeappstore/sdk/hooks. Requires React 18+ as a peer dependency.

import { useAuth, useTheme } from '@freeappstore/sdk/hooks';

function App() {
  const { user, loading, signIn, signOut, deleteAccount } = useAuth(fas);
  const { theme, preference, setPreference } = useTheme();
  // ...
}

App shell

Every app wraps itself in Shell from @freeappstore/sdk/ui: the topbar (app name, text size, theme, avatar link to a profile page), the app's main navigation from nav, and an error boundary, loading fallback, toast region, offline banner and skip link around your screens. The avatar opens /profile in both auth states: GitHub and Google sign-in choices when signed out, account and preferences when signed in. Pass profileHref for an app-owned profile route. The optional sign-in gate (requireAuth) protects app content while keeping the profile page accessible.

import { type NavItem, PageHeader, Shell, useToast } from '@freeappstore/sdk/ui';

const NAV: NavItem[] = [
  { label: 'Notes', href: '/', title: 'Notes' },
  { label: 'Tags', href: '/tags', title: 'Tags — Notes' },
];

<Shell app={fas} appName="Notes" nav={NAV} onNavigate={navigate}>
  <Screen />
</Shell>

// In a screen: its single h1, and feedback through the shell
<PageHeader title="Tags" />
const toast = useToast(); toast.show('Saved', { variant: 'success' });

onNavigate makes nav clicks client-side (scroll and focus are handled on route changes); without it the items are ordinary links. FasShell is the same component under its older name. Full reference: docs/ui.md.

Omit profileHref and the avatar opens the built-in /profile page (ProfilePage) in both auth states. Set profileHref for an app-owned route and render both states yourself. headerActions is the header control slot. A custom frame uses ToastProvider, AccountSection, and the router helpers instead of patching the Shell DOM. ProfileMenu is a legacy dropdown. Shell does not mount it. Layout ownership: composable starter contract. Migration: Migrating from DOM overrides.

UI Components

Drop-in React components. Import from @freeappstore/sdk/ui. Components use CSS custom properties (--ink, --accent, etc.) to blend into your app's theme.

import {
  Shell, PageHeader, NavBar, useToast, useDocumentTitle, useOnline,
  Avatar, SignInButton, ThemeToggle, ProfileMenu, ProfilePage,
  Spinner, Badge, Card, Tabs, Modal, ConfirmDialog, EmptyState,
  ProgressBar, SearchInput, ListRow, ErrorBoundary, KeyPrompt,
} from '@freeappstore/sdk/ui';

// Building blocks
<Spinner size={24} />
<Badge variant="success">Live</Badge>
<Card onClick={fn}>content</Card>
<Tabs tabs={[{key:'a',label:'A'}]} active="a" onChange={setTab} />
<Modal open={show} onClose={close} title="Settings">content</Modal>
<ConfirmDialog open={show} onConfirm={ok} onCancel={cancel} title="Delete?" message="Sure?" variant="danger" />
<EmptyState message="Nothing here yet" />
<ProgressBar value={75} label="Progress" />
<SearchInput value={q} onChange={setQ} />
<ListRow title="Item" subtitle="details" onClick={fn} />
<ErrorBoundary>{children}</ErrorBoundary>
<KeyPrompt app={fas} provider="openai" providerName="OpenAI" />

Full docs: freeappstore.online/skills.md

For AI Agents

Building with Claude Code, Cursor, or another AI tool? Two options:

MCP Server (recommended) -- your agent gets tools to check deploys, look up SDK docs, and list apps:

{
  "mcpServers": {
    "freeappstore": {
      "command": "npx",
      "args": ["mcp-remote", "https://mcp.freeappstore.online/mcp"]
    }
  }
}

SKILLS.md -- full platform guide (tech stack, SDK reference, CLI, deploy flow, compliance):

https://freeappstore.online/skills.md

License

MIT.

Tailwind styling (SDK 0.16+)

React UI requires Tailwind 4. SDK styles are compiled by your app; they are no longer injected at runtime. In web/src/index.css, after the Tailwind import, add:

@source "../node_modules/@freeappstore/sdk/dist";
@source "../../node_modules/@freeappstore/sdk/dist";

Paths are relative to the stylesheet and cover web-local and root-hoisted installs. Also add the token @theme inline mapping and animation keyframes from the styling guide. Both official templates include the complete setup. Floating UI uses opaque --popover (default var(--paper)); --panel is opaque and glass is opt-in.

The connected draft hook and example are a supported SDK recipe. The canonical generated connected template is not wired to this feature in #350; carry starter wiring and adoption into #352 rather than claiming existing generated templates changed.

Temporary photos (fas.photos)

const photo = await fas.photos.upload(file, { visibility: 'public' });
// photo.url works in <img>; photo.expiresAt is an epoch timestamp in milliseconds.
await fas.photos.delete(photo.id);

Omit visibility for private photos; use fas.photos.download(id) to retrieve those with authentication. fas.photos.list() returns your live photos. The SDK resizes to 640 px on the longest edge, re-encodes as JPEG (stripping metadata) and targets 100 KB or less. The server independently enforces a 200,000-byte maximum, JPEG/WebP still images, dimensions and metadata stripping. Every upload expires after 90 days, including private photos. Upload quotas are 20/user/day across all apps, 100 live/user/app and 10,000 live/app, with byte and app daily limits too. This is temporary photo storage, not durable file storage. See the platform contract.

版本与发布记录

@freeappstore/sdk 共发布 130 个版本,首次发布于 2026/05/05;过去 12 个月发布了 130 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.17.1latest2026/11/141 天前
0.17.02026/11/141 天前
0.16.572026/11/141 天前
0.16.562026/11/141 天前
0.16.552026/11/132 天前
0.16.542026/11/132 天前

关于 @freeappstore/sdk 的常见问题

@freeappstore/sdk 是什么?

Browser SDK for FreeAppStore — auth, KV, collections, counters, rooms, proxy, encrypted user keys, roles, friends, email, push reminders, device-local persistence, validated backup and file export, React hooks, and UI components.

如何安装 @freeappstore/sdk?

在项目目录运行 npm install @freeappstore/sdk,或使用 pnpm add / yarn add / bun add。

@freeappstore/sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@freeappstore/sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@freeappstore/sdk 可以商用吗?

@freeappstore/sdk 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@freeappstore/sdk 的下载量有多少?

根据 npm 官方下载统计,@freeappstore/sdk 在 2026/10/16 – 2026/11/14 这 30 天内被下载 14,939 次,日均约 498 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@freeappstore/sdk 还在维护吗?

最新版本 0.17.1 发布于 2026/11/14(1 天前);至今共发布 130 个版本,其中过去 12 个月发布了 130 个;首次发布于 2026/05/05。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。