Imported from kiva/ui (
AGENTS.md). Install upstream withnpx skills add kiva/ui. Copyright stays with the author.
Kiva UI - AI Agent Instructions
Project Overview
Kiva UI is a Vue 3 SSR (Server-Side Rendering) application for Kiva.org's lending platform. It uses Vite for builds, Apollo Client for GraphQL, and Tailwind CSS for styling.
Vue 3 Best Practices
Vue 3 API Usage
Either authoring style works with Apollo prefetching. SSR prefetch discovery follows the components option a component registers, and for a <script setup> component, which cannot register one, the build step attaches its imported children instead.
Only the Options API can author an apollo block. A <script setup> component fetches the data it needs for the initial render through a composable instead — useApolloQuery with an exported preFetchOperations, as shown under "Data Fetching in Composables" below.
⚠️ Options API + Composition API Nesting Warning
Deeply nested component chains can have issues when parent components use Options API and children use Composition API (or vice versa). When working with nested components:
- Verify
inject/providechains work correctly across API boundaries - Test the full component hierarchy, not just the component you're editing
- Watch for
undefinedinjection values in deeply nested Composition API components
Use Composition API where appropriate
- Use composables over mixins for new code. Composables live in
src/composables/ - Mixins (
src/plugins/*-mixin.js) exist for legacy code but should not be used for new features - When refactoring, migrate mixins to composables where practical
Architecture
Rendering Modes
- SSR Pages: Most pages render server-side with data prefetching via
src/server-app-render.js - CDN-Cached Pages: Routes with
meta: { useCDNCaching: true }skip user-specific data on SSR for caching - ESI Fragments: Edge-side includes for cached page components via
src/server-esi-render.js
Key Directories
src/pages/- Route page components (one folder per feature)src/components/- Reusable components organized by feature (e.g.,WwwFrame/,LoanCards/,Checkout/)src/components/Kv/- Internal UI primitives (prefix:Kv)src/graphql/- GraphQL queries/mutations organized by typesrc/composables/- Vue 3 composition API utilitiessrc/plugins/- Vue plugins and mixinsserver/- Express server, auth routers, and SSR middleware
GraphQL Data Fetching Pattern
Component Apollo Block (Options API)
Components use an apollo block with inject: ['apollo', 'cookieStore']:
export default {
inject: ['apollo', 'cookieStore'],
apollo: {
preFetch: true, // Enable SSR prefetch
query: myQuery,
preFetchVariables({ route }) {
return { id: route.query.id }; // SSR variables (no `this`)
},
variables() {
return { id: this.$route.query.id }; // Client variables
},
result({ data }) {
this.myData = data?.someProperty;
},
}
}
For multiple queries, use an array of operation objects. See src/graphql/README.md for advanced patterns.
Data Fetching in Composables
Composables read state needed for the initial render through useApolloQuery, never through the apollo client directly. The apollo client is still used directly for mutations and for imperative fetches in event handlers.
import useApolloQuery from '#src/composables/useApolloQuery';
const operation = { query: myQuery };
// Exporting an operation here registers it: every component that imports this
// composable prefetches it
export const preFetchOperations = [operation];
export default function useMyThing() {
const { result, loading } = useApolloQuery(operation);
// Derive state from result; loading distinguishes "not loaded yet" from real values
}
- Registered operations take the same options as component
apolloblock operations and are prefetched alongside them by the same implementation, with the same variables and failure behavior. Composables that use other composables need nothing more. useApolloQuery(operation, variables)returnsresult/loading/errorrefs. It reads the prefetched value from the cache and never fetches during server render; a dev warning names the cause of any server cache miss, and the client subscription loads and follows the value. The optionalvariablesargument is a plain object or a reactive getter over the composable's own state.- Never initialize a ref to a guessed value to cover the load window; derive from
resultandloadingso unknown state is representable.
See src/graphql/README.md ("Composable Data Fetching") for the operation options, the complete list of differences from component blocks, and what the warnings mean.
Server Rendering Scope Rules
Module scope is worker-scoped, not request-scoped: never store request or user data in module scope. See docs/server-side-rendering.md for the full scope rules.
Styling Conventions
Tailwind CSS
- All Tailwind classes require
tw-prefix (e.g.,tw-flex,tw-mb-4) - Tokens come from
@kiva/kv-tokensviatailwind.config.js - Grid system:
KvGrid,KvPageContainerfrom@kiva/kv-components
Component Library
Directive: Always check
@kiva/kv-componentsfirst before creating custom UI elements.
- Prefer
@kiva/kv-componentsfor buttons, forms, modals, layout, and common UI patterns - Available components:
KvButton,KvSelect,KvTextInput,KvGrid,KvPageContainer,KvMaterialIcon,KvThemeProvider, etc. - Internal
Kv*components insrc/components/Kv/are either project-specific or deprecated and supplement the shared library
Route Definitions
Routes live in src/router/routes.js with lazy loading:
{
path: '/my-page',
component: () => import('#src/pages/MyFeature/MyPage'),
meta: {
authenticationRequired: true,
excludeFromStaticSitemap: true,
}
}
Authentication
- Route meta
authenticationRequired: trueenforces login - Route meta
activeLoginRequired: truerequires recent login - Auth handled via
src/util/authenticationGuard.js
Analytics Tracking
Use the directive for click tracking:
<button v-kv-track-event="['Category', 'action-name', 'Label']">
Or inject $kvTrackEvent for programmatic tracking.
Development Commands
nvm use # Use required Node version
npm ci # Install dependencies
npm run dev -- --config=local # Dev server at localhost:8888
npm run dev -- --config=dev-custom-host # With Caddy at https://kiva-ui.local
npm run unit # Vitest unit tests
npm run lint # ESLint + Stylelint + GraphQL lint
npm run fetchSchema # Fetch latest GraphQL schema from the API and update build/schema.graphql
npm run build # Production build
Testing
- Unit tests: Vitest + Testing Library in
test/unit/specs/ - Use
globalOptionsfromtest/unit/specUtils.jsfor mock apollo/cookieStore - E2E tests: Cypress in
cypress/against real environments - Test user-centric behavior, not implementation details
Import Aliases
Use #src/ alias for imports from the src directory:
import WwwPage from '#src/components/WwwFrame/WwwPage';
import myQuery from '#src/graphql/query/myQuery.graphql';
Code Style & Linting
Editor Configuration
- Indentation: Tabs (size 4)
- Line length: Max 120 characters
- Line endings: LF only
- Charset: UTF-8
- YAML files: Exception—use spaces (2-space indent)
ESLint Key Rules
- Vue components must have explicit
nameproperty (vue/require-name-property) - Component file names must match component name case-sensitively (
vue/match-component-file-name) - Import extensions: Omit
.js/.vue/.mjsextensions in imports; ESLint enforces this - GraphQL operations must be named (
graphql/named-operations)—anonymous operations will fail - All GraphQL types must include
idandkeyfields (graphql/required-fields) - Vue templates: No self-closing normal HTML elements; use paired tags
- Debugging:
debuggerstatements error in production, allowed in development - Console statements: Only
console.error()allowed in production; others cause warnings
Pre-commit Hooks
Husky is configured to run linting on commit:
npm run lint # Run before committing
This runs ESLint, Stylelint, and GraphQL linting. Fix violations before committing.