본문으로 건너뛰기
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.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

연동 도구Supabase
설치 방법 보기

포함된 파일(1)

  • SKILL.md13.1 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Provider & Initialization Rules

<!-- rules:stamp -->

Rules from common-docs/systems/architecture/frontend/STATE.md — edit there, never here (python3 common-docs/meta/scripts/one_home.py --write).

  • Server Component shell with fixed-dimension frames; dynamic content streams into sized Suspense holes, and skeletons match the final dimensions, so layout shift is zero. "use client" goes on the smallest island.
  • A Server Component cannot use dynamic({ ssr: false }); the heavy widget below a thin server shell owns its splitting inside a "use client" child. Never stack ssr:false boundaries down one render path (a waterfall), never mass-convert React.lazy to next/dynamic, and a dynamic import that is always rendered is cost with no deferral: gate it. A user-triggered chunk always has a loading and an error boundary.
  • Nothing fetches until needed: no provider, context or hook calls Supabase, an API or IndexedDB on mount; providers mount as empty shells and the consumer that needs data triggers its load. Every removal from boot ships its on-click load.
  • Nothing runs twice: never wrap a second <XProvider> for a context the global tree already provides. Redux for all new state; context providers are legacy and are not expanded.
  • No client-side Supabase auth calls; identity is in Redux. A read that needs a session never leaves the client without one, and a refusal is never silent: use the existing session barrier (utils/supabase/sessionBarrier.ts), never hand-roll a session check, subscription or retry.
  • Choose the sharing tier by rule: database leaf (renders one shape's data, no deploy) then @ai-matrx/* package (every app needs it, identical everywhere) then host (this app's wiring, never shared "just in case"). Package source never imports host aliases (@/), and a package change is the first option, never a copy or workaround.
  • A new subdomain app is a project, not an architecture change; cross-host auth rides the .aimatrx.com cookie and hard navigation between hosts is fine.
  • A route group's shell posture is deliberate: (kiosk), (meet), (link) and (lab) are ungated or shell-less on purpose; never add auth or AppShell to them to match (core).
<!-- rules:stamp-end -->

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)

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

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월 21일 업데이트

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월 21일 업데이트

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월 21일 업데이트

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월 21일 업데이트

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월 21일 업데이트

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월 21일 업데이트

armanisadeghi의 스킬 모두 보기

이 스킬의 문제 신고