# How to Customize TaxHacker Settings: Complete Developer Guide

> Learn to customize TaxHacker settings for developers. This guide details using Prisma database, getSettings, updateSettings, and Zod validation for seamless configuration.

- Repository: [Vasily Zubarev/TaxHacker](https://github.com/vas3k/TaxHacker)
- Tags: how-to-guide
- Published: 2026-04-01

---

**TaxHacker stores all user-specific configuration as key-value pairs in a Prisma database table, exposing them through the `getSettings` and `updateSettings` functions in [`models/settings.ts`](https://github.com/vas3k/TaxHacker/blob/main/models/settings.ts), with UI forms in `components/settings/` that validate input via Zod and persist changes through `saveSettingsAction`.**

TaxHacker is an open-source tax management application that stores user preferences and LLM provider configurations in a flexible, extensible settings architecture. Customizing TaxHacker settings involves understanding three distinct layers: the Prisma persistence layer, the default values fallback system, and the React server action UI layer. This guide walks through the exact implementation details found in the [vas3k/TaxHacker](https://github.com/vas3k/TaxHacker) repository to help you modify defaults, add new configuration options, or programmatically override TaxHacker settings.

## Understanding the TaxHacker Settings Architecture

The application implements a three-layer configuration system that separates storage, defaults, and presentation concerns.

**Persistence Layer** ([`models/settings.ts`](https://github.com/vas3k/TaxHacker/blob/main/models/settings.ts)): Reads and writes raw `code → value` pairs using Prisma.

**Defaults Layer** ([`models/defaults.ts`](https://github.com/vas3k/TaxHacker/blob/main/models/defaults.ts)): Supplies fallback values when a user-specific key is missing from the database.

**UI/Actions Layer** ([`forms/settings.ts`](https://github.com/vas3k/TaxHacker/blob/main/forms/settings.ts), `components/settings/*.tsx`, `app/(app)/settings/actions.ts`): Renders configuration forms, validates user input through Zod schemas, and calls server functions to persist changes.

All layers connect through the **settings** API endpoint and the **saveSettingsAction** server function, which revalidates the `/settings` path after successful updates.

## Reading and Writing TaxHacker Settings Programmatically

### The Settings Model ([`models/settings.ts`](https://github.com/vas3k/TaxHacker/blob/main/models/settings.ts))

The core persistence logic resides in [`models/settings.ts`](https://github.com/vas3k/TaxHacker/blob/main/models/settings.ts), which defines a `SettingsMap` type and two primary functions:

```typescript
export type SettingsMap = Record<string, string>;

export const getSettings = cache(async (userId: string): Promise<SettingsMap> => {
  const settings = await prisma.setting.findMany({ where: { userId } });
  return settings.reduce((acc, s) => {
    acc[s.code] = s.value || "";
    return acc;
  }, {} as SettingsMap);
});

export const updateSettings = cache(async (userId: string, code: string, value: string | undefined) => {
  return await prisma.setting.upsert({
    where: { userId_code: { code, userId } },
    update: { value },
    create: { code, value, name: code, userId },
  });
});

```

**`getSettings`** performs a memoized, cached read of all settings rows for the specified user, returning a plain JavaScript object. **`updateSettings`** executes an `upsert` operation using the composite unique key `userId_code`, creating the row if it does not exist or updating the existing value.

### Default Values and Fallbacks ([`models/defaults.ts`](https://github.com/vas3k/TaxHacker/blob/main/models/defaults.ts))

When a setting is absent from the database, TaxHacker falls back to the `DEFAULT_SETTINGS` array:

```typescript
export const DEFAULT_SETTINGS = [
  { code: "default_currency", value: "EUR" },
  { code: "default_type", value: "expense" },
  // … additional defaults …
];

```

During initial user creation, the `createUserDefaults` function (located in the same file) copies these defaults into the database. To establish a new global default for future users, add an entry to this array.

## Customizing the TaxHacker Settings UI

### Global Settings Forms

The global configuration interface lives in [`components/settings/global-settings-form.tsx`](https://github.com/vas3k/TaxHacker/blob/main/components/settings/global-settings-form.tsx). This client component receives the `SettingsMap` as props and renders form fields using the current saved value (or the default):

```tsx
<FormSelectCurrency
  title="Default Currency"
  name="default_currency"
  defaultValue={settings.default_currency}
  currencies={currencies}
/>

```

Each input uses the `defaultValue` prop to display the persisted setting, ensuring the UI reflects the database state immediately.

### LLM Provider Configuration

The LLM settings interface in [`components/settings/llm-settings-form.tsx`](https://github.com/vas3k/TaxHacker/blob/main/components/settings/llm-settings-form.tsx) handles dynamic provider management:

```tsx
<input type="hidden" name="llm_providers" value={providerOrder.join(",")} />
<FormTextarea
  title="Prompt for File Analysis Form"
  name="prompt_analyse_new_file"
  defaultValue={settings.prompt_analyse_new_file}
/>

```

Users reorder providers via drag-and-drop, which updates the hidden `llm_providers` field. The component dynamically generates inputs for API keys and models based on the `PROVIDERS` constant from [`lib/llm-providers.ts`](https://github.com/vas3k/TaxHacker/blob/main/lib/llm-providers.ts).

### Server-Side Validation and Persistence

The `saveSettingsAction` function in `app/(app)/settings/actions.ts` processes form submissions:

```typescript
export async function saveSettingsAction(_prevState, formData) {
  const user = await getCurrentUser();
  const validated = settingsFormSchema.safeParse(Object.fromEntries(formData));
  if (!validated.success) return { success: false, error: validated.error.message };
  for (const key in validated.data) {
    const value = validated.data[key as keyof typeof validated.data];
    if (value !== undefined) await updateSettings(user.id, key, value);
  }
  revalidatePath("/settings");
  return { success: true };
}

```

This action validates incoming data against the Zod schema defined in [`forms/settings.ts`](https://github.com/vas3k/TaxHacker/blob/main/forms/settings.ts), iterates through each field to call `updateSettings`, and triggers Next.js path revalidation to refresh cached settings pages.

## Consuming TaxHacker Settings in Business Logic

Backend utilities transform the raw `SettingsMap` into typed configuration objects. The `getLLMSettings` function (also in [`models/settings.ts`](https://github.com/vas3k/TaxHacker/blob/main/models/settings.ts)) demonstrates this pattern:

```typescript
export function getLLMSettings(settings: SettingsMap) {
  const priorities = (settings.llm_providers || "openai,google,mistral,openai_compatible")
    .split(",")
    .map(p => p.trim())
    .filter(Boolean);
  const providers = priorities.map(provider => {
    if (provider === "openai") { /* ... */ }
    // other providers ...
  }).filter(p => p !== null);
  return { providers };
}

```

Components requiring LLM access call `getLLMSettings(settings)` to retrieve an ordered list of active providers with their respective API keys and model configurations.

## Step-by-Step Guide to Customizing TaxHacker Settings

### Adding a New Setting Field

To extend the configuration surface with a custom setting:

1. **Define the schema**: Add a Zod entry in [`forms/settings.ts`](https://github.com/vas3k/TaxHacker/blob/main/forms/settings.ts) (e.g., `custom_feature_flag: z.boolean().optional()`).

2. **Update defaults**: Append the new key and default value to `DEFAULT_SETTINGS` in [`models/defaults.ts`](https://github.com/vas3k/TaxHacker/blob/main/models/defaults.ts).

3. **Build the UI**: Create or modify a component in `components/settings/` (such as [`global-settings-form.tsx`](https://github.com/vas3k/TaxHacker/blob/main/global-settings-form.tsx)) to render the input using `FormCheckbox`, `FormSelect`, or `FormInput` components.

4. **Persist**: The existing `saveSettingsAction` automatically handles validation and storage for any field defined in the Zod schema.

### Modifying LLM Provider Priority Programmatically

To override the provider order without manual UI interaction:

```typescript
await updateSettings(userId, "llm_providers", "google,openai,mistral,openai_compatible");

```

The `getLLMSettings` function will respect this order on the next request, prioritizing Google over OpenAI.

### Accessing Settings in Backend Code

Fetch and utilize settings in server components or API routes:

```typescript
import { getSettings, getLLMSettings } from "@/models/settings";

const settings = await getSettings(user.id);
const { providers } = getLLMSettings(settings);
const primaryProvider = providers[0]; // Highest priority LLM configuration

```

## Practical Code Examples for TaxHacker Settings

### Fetching Settings in a Server Component

```tsx
import { getSettings } from "@/models/settings";

export default async function SettingsPage() {
  const user = await getCurrentUser();
  const settings = await getSettings(user.id);

  return <pre>{JSON.stringify(settings, null, 2)}</pre>;
}

```

### Creating a Custom Settings API Route

```typescript
// app/api/custom-settings/route.ts
import { updateSettings } from "@/models/settings";

export async function POST(req: Request) {
  const { userId, key, value } = await req.json();
  await updateSettings(userId, key, value);
  return new Response(JSON.stringify({ ok: true }), { status: 200 });
}

```

### Initializing LLM Clients from TaxHacker Settings

```typescript
import { getSettings, getLLMSettings } from "@/models/settings";
import { OpenAIProvider, GoogleProvider } from "@/lib/llm-providers";

export async function makeLLMClient(userId: string) {
  const settings = await getSettings(userId);
  const { providers } = getLLMSettings(settings);
  const primary = providers[0];

  switch (primary.provider) {
    case "openai":
      return new OpenAIProvider(primary.apiKey, primary.model);
    case "google":
      return new GoogleProvider(primary.apiKey, primary.model);
    default:
      throw new Error("Unsupported provider");
  }
}

```

## Summary

- **TaxHacker stores all configuration as key-value pairs** in the Prisma `setting` table, accessed via `getSettings` and mutated via `updateSettings` in [`models/settings.ts`](https://github.com/vas3k/TaxHacker/blob/main/models/settings.ts).
- **Default values** are defined in the `DEFAULT_SETTINGS` array within [`models/defaults.ts`](https://github.com/vas3k/TaxHacker/blob/main/models/defaults.ts) and copied to new users during onboarding.
- **The UI layer** ([`global-settings-form.tsx`](https://github.com/vas3k/TaxHacker/blob/main/global-settings-form.tsx), [`llm-settings-form.tsx`](https://github.com/vas3k/TaxHacker/blob/main/llm-settings-form.tsx)) renders forms that submit to `saveSettingsAction`, which validates data using Zod schemas from [`forms/settings.ts`](https://github.com/vas3k/TaxHacker/blob/main/forms/settings.ts) before persisting.
- **Backend logic** consumes the raw `SettingsMap` through helpers like `getLLMSettings` to transform string values into typed configuration objects for API clients.
- **Customization requires minimal changes**: add a key to the schema, update defaults if necessary, and the existing architecture handles persistence and retrieval automatically.

## Frequently Asked Questions

### How are TaxHacker settings stored in the database?

TaxHacker persists settings in a Prisma table named `setting` using a composite unique key of `userId` and `code`. Each row stores a single configuration value as a string, allowing the application to handle arbitrary configuration keys without database schema migrations. The `updateSettings` function in [`models/settings.ts`](https://github.com/vas3k/TaxHacker/blob/main/models/settings.ts) performs upsert operations against this table.

### What is the difference between `getSettings` and `updateSettings`?

`getSettings` is a cached, read-only function that returns a `SettingsMap` object containing all key-value pairs for a specific user. `updateSettings` is a write operation that accepts a user ID, setting code, and value, then performs a Prisma `upsert` to either create a new row or update an existing one. Both functions utilize Next.js `cache` for memoization within the same request lifecycle.

### How do I add a new default setting for all TaxHacker users?

Add the new setting to the `DEFAULT_SETTINGS` array in [`models/defaults.ts`](https://github.com/vas3k/TaxHacker/blob/main/models/defaults.ts) with a `code` and `value` property. Existing users will not automatically receive this default; you must either run a script calling `updateSettings` for each user or invoke the `createUserDefaults` logic manually. New users automatically receive the updated defaults during account creation.

### Can I change TaxHacker settings without using the web interface?

Yes. Import `updateSettings` from `@/models/settings` into any server-side context—such as a Next.js API route, a background job script, or a custom server action—and call it with the target user ID, setting code, and desired value. This approach bypasses the Zod validation performed by `saveSettingsAction`, so ensure data integrity before persisting.