How to Customize TaxHacker Settings: Complete Developer Guide
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, 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 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): Reads and writes raw code → value pairs using Prisma.
Defaults Layer (models/defaults.ts): Supplies fallback values when a user-specific key is missing from the database.
UI/Actions Layer (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)
The core persistence logic resides in models/settings.ts, which defines a SettingsMap type and two primary functions:
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)
When a setting is absent from the database, TaxHacker falls back to the DEFAULT_SETTINGS array:
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. This client component receives the SettingsMap as props and renders form fields using the current saved value (or the default):
<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 handles dynamic provider management:
<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.
Server-Side Validation and Persistence
The saveSettingsAction function in app/(app)/settings/actions.ts processes form submissions:
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, 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) demonstrates this pattern:
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:
-
Define the schema: Add a Zod entry in
forms/settings.ts(e.g.,custom_feature_flag: z.boolean().optional()). -
Update defaults: Append the new key and default value to
DEFAULT_SETTINGSinmodels/defaults.ts. -
Build the UI: Create or modify a component in
components/settings/(such asglobal-settings-form.tsx) to render the input usingFormCheckbox,FormSelect, orFormInputcomponents. -
Persist: The existing
saveSettingsActionautomatically 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:
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:
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
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
// 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
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
settingtable, accessed viagetSettingsand mutated viaupdateSettingsinmodels/settings.ts. - Default values are defined in the
DEFAULT_SETTINGSarray withinmodels/defaults.tsand copied to new users during onboarding. - The UI layer (
global-settings-form.tsx,llm-settings-form.tsx) renders forms that submit tosaveSettingsAction, which validates data using Zod schemas fromforms/settings.tsbefore persisting. - Backend logic consumes the raw
SettingsMapthrough helpers likegetLLMSettingsto 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 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →