本文へ移動
cccskills
無料GitHub で公開

provider-and-init-rules

Startup rules for the app shell, layouts, and global providers. Use when editing app/Providers.tsx, app/DeferredSingletonWrapper.tsx / DeferredSingletonCore.tsx, a layout.tsx, StoreProvider, or a global provider; adding startup fetches or mount-time useEffects high in the tree; or reading auth state (isAdmin, isGuest, fingerprintId) client-side.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md10.9 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Provider & Initialization Rules

These rules govern everything in the upper layers of the app: providers, layouts, shell components, and anything that runs on every page load. Violations here cost every user on every page.

Cardinal Rules

  1. Nothing fetches until needed. No provider, context, or hook may call Supabase, an API, IndexedDB, or any async data source on mount. Providers mount as empty shells. Data loading is triggered by the consumer that actually needs it.

  2. Nothing runs twice. If the global provider tree already provides a context, child routes and components MUST NOT wrap their own duplicate instance. Use initialize() from the existing context, never a second <XProvider>.

  3. Redux over providers. Our internal policy is to use Redux for all new state. Context providers are legacy. When touching an existing provider, do not expand it — plan its migration to Redux. If there is absolutely no alternative and a legacy provider must stay, it still obeys rule 1.

  4. No client-side Supabase auth calls. User identity is already in Redux. Use selectors. The only legitimate Supabase auth access is server-side (Server Components, API routes, proxy.ts).

  5. No authenticated read leaves the client before the session is attached. You inherit this — see THE CLIENT BOOT LAW below. Never hand-roll a session check, an auth subscription, or a retry around a Supabase read.


THE CLIENT BOOT LAW (DD-237)

A read that needs a session never leaves this client without one, and a read refused for having none is never silent. You get this for free and you must not reimplement it.

Why it exists

supabase-js attaches the caller's JWT inside its own fetch. When auth.getSession() yields nothing it silently sends the PUBLISHABLE key instead, PostgREST runs the statement as anon, and anon holds no grant on any application table in this database. The read comes back 42501 permission denied at HTTP 401 — a refusal, not an empty list, indistinguishable on the wire from a real grant gap. Measured on production over the 48 h to 2026-09-14: 59 such rows across 28 relations, none retried, none shown to anyone. Two live shapes, both covered: a read racing the session at boot, and a long-lived background tab whose session lapsed underneath it.

What you inherit

utils/supabase/sessionBarrier.ts, installed in the ONE proxy the browser client is wrapped in (lib/diagnostics/supabaseErrorCapture.ts, bound in utils/supabase/authCookie.ts). Every .from() / .rpc() / .schema() from @/utils/supabase/client passes through it:

WaitWhen the auth cookie says this browser is signed in but no session is in hand, the request waits — bounded by SESSION_ATTACH_BUDGET_MS, and skipped entirely on the healthy path, which stays synchronous.
Retry onceA 401 refusal carrying 42501 / PGRST301 / 28000 re-resolves the session and replays the request exactly once. Safe for writes: a 42501 is raised before the statement does anything.
ScreamA barrier that fires says so in the Error Inspector with the remedy, and every Supabase capture is stamped with sessionState → ops.system_error.context.session_state.

The four rules

  1. Get your client from @/utils/supabase/client. A client you build yourself (createBrowserClient, supabaseNext.browserClient()) inherits neither the barrier nor error capture. Guard: pnpm check:session-first-reads.
  2. Never write your own session gate. No if (!userId) return before a read, no onAuthStateChange subscription to re-run a query, no hand-rolled retry. Need to know when a session exists? whenSessionAttached() / hasAttachedSession() from the barrier — one subscription per tab, and the ONE signal the platform publishes.
  3. A 42501 at 403 is NOT a session problem. It is a real grant gap for authenticated. The barrier deliberately leaves it alone; so should you. Fix the grant.
  4. A door that a caller with no account reaches is declared by name in utils/supabase/anonymousByDesignDoors.ts, mirroring platform.client_callable_door.anonymous_callers, WITH its purpose. That list only removes a wait — it can never widen access, and the database still answers.

User Identity — Client Side

User state is populated at the server boundary and hydrated into Redux before any client code runs. Never call supabase.auth.getUser(), supabase.auth.getSession(), or any Supabase auth method from a client component.

Selectors (@/lib/redux/slices/userSlice)

NeedSelectorReturns
Is this a logged-in user?selectIsAuthenticatedboolean (!!state.user.id)
Is this an admin?selectIsAdminboolean
Is this a guest (fingerprint only)?selectFingerprintIdstring | null — non-null means guest
Auth init complete?selectAuthReadyboolean — true once user OR fingerprint is set
User IDselectUserIdstring | null
Display nameselectDisplayNamestring (derived, memoized)
Full context objectselectUserContext{ user, isAuthenticated, isAdmin }

Guest detection pattern:

const isAuthenticated = useAppSelector(selectIsAuthenticated);
const fingerprintId = useAppSelector(selectFingerprintId);
const isGuest = !isAuthenticated && !!fingerprintId;

The Idle Scheduler (@/utils/idle-scheduler)

All non-critical initialization is deferred through the IdleScheduler — a module-level singleton (not React context) that waits for the browser to be truly idle after the page paints.

Priority Scale

PriorityWhen it runsUse for
1First of the deferred batchAnalytics init, critical measurements
2HighPreferences sync, service worker registration
3NormalNon-critical UI hydration, 3rd-party widgets
4LowTelemetry, background sync
5Absolute lastBroker registration, admin features, speculative prefetch

Hooks

// Fire-and-forget deferred work (zero re-renders)
useIdleTask(key, priority, callback);

// Gate rendering until idle flush completes (one re-render: false → true)
const ready = useIdleReady();

// Register work AND get notified when done
const { ready } = useIdleGate(key, priority, callback);

Where deferred work lives

FilePurpose
app/DeferredSingletonWrapper.tsx → DeferredSingletonCore.tsxDeferred background logic: broker registration (p5), preferences load (p2), PostHog identify (p3), plus UI singletons gated behind useIdleReady()
features/shell/islands/DeferredIslands.tsxHeavy layout-level UI chunks (Canvas, Messaging, VoicePad, WindowTray) gated behind useIdleReady()

Provider Rules in Detail

Empty-Shell Pattern

Every provider in app/Providers.tsx MUST mount as a zero-cost shell: create context, set default state, render children. No useEffect that fetches. No realtime subscriptions. No timers.

When data is needed, the provider exposes an idempotent initialize() function:

// Inside the provider
const [initialized, setInitialized] = useState(false);

const initialize = useCallback(() => {
  if (initialized) return;       // Idempotent — safe to call many times
  setInitialized(true);
  // NOW fetch, subscribe, poll, etc.
}, [initialized]);

// Expose via context value
const value = { ...otherStuff, initialize, initialized };

Consumers call initialize() on mount when they actually need the data:

function TasksPage() {
  const { initialize } = useTaskContext();
  useEffect(() => { initialize(); }, [initialize]);
  // ...
}

No Duplicate Provider Instances

If a provider already exists in the global tree (app/Providers.tsx), child components MUST NOT wrap their own <XProvider>. This causes duplicate state, duplicate fetches, and duplicate realtime channels.

Wrong:

function MyFeaturePage() {
  return (
    <TaskProvider>        {/* DUPLICATE — already in global tree */}
      <TaskContent />
    </TaskProvider>
  );
}

Right:

function MyFeaturePage() {
  const { initialize } = useTaskContext();
  useEffect(() => { initialize(); }, [initialize]);
  return <TaskContent />;
}

Adding a New Provider (Legacy — Only When Unavoidable)

  1. Create it as an empty shell with an initialize() method.
  2. Add it to app/Providers.tsx in the appropriate position.
  3. Do NOT add eager useEffect calls.
  4. Document why Redux was not viable (this should be rare).
  5. Add a // TODO: migrate to Redux comment at the top of the file.

Preferred: Adding New State via Redux

  1. Create a slice in lib/redux/slices/ or features/<name>/redux/.
  2. Add typed selectors (memoized with createSelector when derived).
  3. If async initialization is needed, use a thunk dispatched via useIdleTask in DeferredSingletonCore.tsx or triggered by the first consumer that needs the data.

What Belongs Where

CategoryLocationRuns when
Redux store creationStoreProviderImmediately (synchronous, zero network)
Theme detectionThemeProviderImmediately (reads cookie, no network — prevents FOUC)
Schema hydrationSchemaProviderImmediately (server-passed data, zero network)
Query client creationReactQueryProviderImmediately (synchronous, zero network)
User preferences loadDeferredSingletonCoreIdle priority 2
Broker registrationDeferredSingletonCoreIdle priority 5
PostHog identifyDeferredSingletonCoreIdle priority 3
Admin debug toolsDeferredSingletonCore → AdminFeatureProviderIdle (rendered after useIdleReady())
Heavy layout islandsDeferredIslandsAfter useIdleReady() returns true
Feature data (tasks, transcripts, files, audio recovery)Feature consumer calls initialize()When user navigates to or opens that feature

Checklist Before Merging

  • No useEffect in any provider that fetches data, opens subscriptions, or starts timers on mount
  • No duplicate <XProvider> wrappers — only the global instance in app/Providers.tsx
  • No supabase.auth.* calls from client components — use Redux selectors
  • Any new deferred work uses useIdleTask / useIdleReady with appropriate priority
  • New state uses Redux, not a new context provider (document exceptions)
  • initialize() on all lazy providers is idempotent (safe to call N times)

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Admin debug system wiring for the floating AdminIndicator's live debug data and Copy Full Context. Use when adding debug visibility to a route or feature, wiring useDebugContext or the debug panel, capturing console errors, or enabling the copy-context workflow for a page.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月13日 更新

Compact two-icon Copy / Copy-for-AI controls (components/agent-copy). Use when adding copy buttons to a row, card, list, or record; merging duplicate Copy/JSON/AI controls; continuing the copy rollout; or writing a Copy-for-AI payload. NOT for markdown content actions (use rich-document-actions).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月13日 更新

Disclosing a surface's existing fixed AI jobs in the shell's top Agents menu. Use when a page, panel, overlay, or window already runs a mandate behind a button, assist, automatic action, or mode; when the agent-disclosure guard names a file; or during a surface check.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月13日 更新

The watch-fix-rerun method for making an existing platform agent or automated process efficient and correct: baseline its ledger, run one unit yourself, fix the class behind every wasted call, rerun, record. Use when asked to improve, optimize, watch, or 'make efficient' an agent, a sandbox session, a sync job, or any recurring automated process.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月13日 更新

Redux state for agent execution and firing agent shortcuts. Use when editing features/agents/redux/, building agent UI, creating a conversation, touching assembleRequest or NDJSON stream state, wiring agent-state selectors, adding a per-conversation capability, or triggering a shortcut from a button, menu, or code (useShortcutTrigger, launchAgentExecution).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月13日 更新

The provision for a call site: the exhaustive menu of values that place in the code can realistically produce. Use before creating or fixing a mandate or agent that reasons about more than its own input, when deciding whether an agent can answer at all with what it is sent, or when its output 'looks right' but nobody checked it could be. NOT for building the agent (use create-agent).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月13日 更新

armanisadeghi のスキルをすべて見る

このスキルの問題を報告する