# Brian Kimball — full markdown corpus

> Systems Engineer and Developer. Writing about IBM i modernization, full-stack web development, network infrastructure, and building tools from the metal up.

Personal website and developer resources of **Brian Kimball**. Generated for AI agents. Canonical HTML site: https://brian-kimball.com/, Developer resources: https://brian-kimball.com/developer/, OpenAPI specification: https://brian-kimball.com/openapi.json

---

# My Custom Pi Configuration for a Multi-Agent Coding Workflow

> The Pi configuration I use for daily development: specialized coding agents, persistent RPC workers, iterative review loops, and a custom orchestration UI.

- Author: Brian Kimball
- Published: 2026-07-20
- Tags: ai, agent-orchestration, systems-architecture, typescript
- Canonical: https://brian-kimball.com/blog/custom-pi-agent-workflow/
- Markdown: https://brian-kimball.com/blog/custom-pi-agent-workflow/index.md

A single coding agent works until its context becomes a record of every abandoned idea, file search, implementation attempt, and test failure from the entire job. Better models delay that failure. They do not remove it.

I use [Pi](https://github.com/earendil-works/pi-mono) as the host for a different shape of workflow: one lead agent coordinates ten specialists, dispatches isolated work, reviews the evidence, and keeps responsibility for the final result. The configuration started with one synchronous `task` tool. It now supports both disposable missions and persistent, steerable RPC workers.

The full configuration is public at [github.com/bskimball/pi](https://github.com/bskimball/pi). The repository contains the agent definitions, lead prompt, extensions, skills, and restore instructions described here. Secret-bearing and machine-local files remain excluded.

The useful part is not the role-playing names. It is the process boundary.

![A historical capture of the terminal UI, still labeled Mono at the time, showing compact tool receipts and an active oracle review](../../assets/pi-tui-screenshot.png)

_This is a historical capture of the interface I use every day, taken before a later rename from Mono to Apex. Successful and failed tools, elapsed time, the active oracle mission, token traffic, model state, MCP connections, and task count remain visible without dumping child transcripts into the lead context._

## The lead agent protects its context for judgment

When one agent owns every phase of a substantial change, its context window becomes a junk drawer:

- reconnaissance notes crowd out the current edit
- failed debugging branches influence later decisions
- implementation details displace the original requirements
- review becomes a reread of the implementer's own reasoning
- release commands get mixed into design and architecture debate

My main system prompt treats the lead as an orchestrator first. It still handles small work directly, but it delegates anything that would consume the context needed for integration and judgment.

The triage is explicit:

- **Inline** for one known file, one small edit, or one direct answer
- **Delegate** for multi-file work, broad investigation, substantial UI work, or difficult debugging
- **Parallelize** only when units are independent
- **Serialize** when units touch the same files or depend on the same decision

Delegation does not transfer ownership of the user's outcome. The lead writes the work order, checks the returned evidence, reconciles disagreements, runs combined validation, and gives the final answer.

That distinction matters. Without it, subagents become an elaborate way to say, "someone else said it passed."

## Ten specialists replace one overloaded mega-prompt

Each specialist is defined in a Markdown file with YAML frontmatter. The frontmatter controls its model route, fallback models, thinking level, tools, skill inheritance, and turn budget. The Markdown body is the role's system prompt.

The roster is intentionally narrow:

1. **advisor** evaluates consequential approaches and tradeoffs before implementation.
2. **artisan** owns substantial UI, layout, visual hierarchy, and interaction polish.
3. **inspector** performs fast, read-only browser verification: screenshots, responsive checks, and focused visual regressions after implementation.
4. **librarian** researches external libraries, framework internals, documentation, and reference implementations.
5. **machinist** handles concrete non-visual implementation such as backend logic, refactors, migrations, and bug fixes.
6. **oracle** provides an independent review or a second opinion on difficult debugging.
7. **picasso** generates image assets from a visual brief.
8. **scout** performs fast, read-only reconnaissance across the local codebase.
9. **scribe** writes and revises long-form technical content.
10. **stevedore** handles bounded operational work such as builds, git mechanics, and platform CLIs.

The names make the terminal easier to scan, but the tool boundaries do the real work. Scout does not need edit access. Librarian should not improvise local patches. Artisan is not interchangeable with a backend coder just because both can write TypeScript. Inspector only verifies the rendered result in a browser; it cannot edit code, so a defect it finds still routes back to artisan or machinist. Oracle is valuable because it inspects the actual diff from a fresh process, not because it produces a ceremonial approval paragraph.

## The topology stays star-shaped

The system has one coordinator. Workers do not grow their own worker farms.

Child processes receive their role prompt and a self-contained work order, not the parent's conversation. Task tools are excluded from child runtimes, so escalation returns to the lead instead of creating an unbounded process tree.

This also keeps shared-state decisions in one place. A worker can report that a push, deploy, deletion, or migration is ready. The lead remains responsible for deciding whether that action should happen.

## Self-contained work orders are the shared memory

A fresh context window is only helpful if the prompt contains enough context to act correctly. My work orders follow a deliberately boring structure:

- **Goal:** the user-visible outcome
- **Scope:** files, directories, behavior, and non-goals
- **Context:** constraints and decisions already made
- **Task:** the exact implementation, investigation, or review request
- **Evidence:** the files, commands, or documentation to inspect first
- **Validation:** the narrowest useful check
- **Return format:** changed files, findings, test results, blockers, and residual risks

The lead is the shared blackboard. A continual-memory extension gives me `memory_list` and `memory_write` for durable notes, split into local memory that is scoped to the current session and global memory that is shared across sessions. It injects a compact overview of that memory into the lead's system prompt each turn. That memory is explicit and manual, not automatic: I write to it deliberately. Workers do not inherit the lead's conversation or its session-local memory; any child that loads the extension gets its own independent view of the global, cross-session memory instead. Because that global memory is supplemental and manual, I still cannot assume it contains the requirements for a given task, so there is no hidden channel that lets a child recover a requirement omitted from its prompt.

That constraint improves the main session too. If I cannot explain a delegated unit clearly, I probably have not decomposed the problem clearly.

## Synchronous tasks are disposable, queued missions

The original orchestration path is still useful. This synchronous path launches a fresh `pi --mode json --no-session` process. It discovers the selected agent, validates the working directory, injects the specialist prompt, streams JSON events into the terminal UI, and returns the final report to the lead. JSON mode applies only to the synchronous `task` runner.

Synchronous tasks have a hard concurrency cap of three. Additional calls queue until a slot opens.

This mode works well for bounded jobs with a known result shape:

- map a subsystem and return relevant file paths
- review a diff without editing it
- implement one isolated change and run a targeted test
- research a library behavior and cite the source

The tradeoff is control. Once dispatched, a synchronous task cannot be steered mid-flight. If the brief is wrong, the lead waits for the task to finish or time out, reads the result, and starts a better-scoped mission.

The synchronous runner can walk the agent's configured model fallbacks for a failed model attempt or a process or result failure. A task abort, timeout, or turn-limit kill stops the attempt loop instead of triggering another fallback. The UI records the active model and whether a fallback was needed, while the lead receives the bounded report rather than the child's full transcript.

## Persistent RPC workers make delegation interactive

Some jobs need a longer relationship than one prompt and one report. Async tasks do not use JSON mode. `task_start` launches `pi --mode rpc` with an isolated, session-backed directory, then communicates with the child over Pi's RPC protocol. That exposes a persistent lifecycle instead of a single call:

- `task_start` launches a specialist and immediately returns a worker handle
- `task_status` shows bounded lifecycle, activity, errors, and pending interactions
- `task_list` shows active and recently settled workers
- `task_send` queues steering, a post-settlement follow-up, or a new prompt
- `task_wait` blocks until the current generation settles
- `task_abort` asks the worker to stop, then escalates if it does not cooperate
- `task_close` reaps the process and releases its concurrency slot
- `task_reply` answers a child UI checkpoint

The async runner has its own cap of three live workers. Unlike the synchronous queue, `task_start` rejects when all three slots are occupied. A settled worker still holds its slot until the lead calls `task_close`.

That explicit cleanup is not cosmetic. Persistent workers preserve useful state, but state has a lifecycle and a cost. Forgetting to close a worker is the orchestration equivalent of leaking a file descriptor.

Steering is also more precise than the name suggests. It does not interrupt a model halfway through inference or cancel a tool already running. A steer message waits for the next model-call boundary. Follow-up messages wait until the worker has fully settled. `task_abort` is the control for work that should actually stop.

The async path is now my default for implementation, uncertain investigation, and anything likely to need correction after the first result. The synchronous path remains better for short, deterministic lookups where persistence would be overhead.

## UI checkpoints cross the process boundary

RPC workers introduced a problem the synchronous runner did not have to solve: a child extension can ask the user a question.

Pi extensions can request a select, confirm, input, or editor dialog. An isolated child cannot display that interaction directly in the parent session, so the async runner records the request as a pending checkpoint. The lead sees it through `task_status` and answers it with `task_reply`, which sends the matching RPC response back to the child.

This is a small feature with a large architectural effect. It means persistent workers can use interactive extensions without pretending every decision was known at launch. The checkpoint is still mediated by the lead, so the star-shaped ownership model remains intact.

## Parallel work needs ownership, not optimism

The most important parallelism rule is not the concurrency limit. It is one writer per worktree.

Read-only agents can investigate in parallel. A librarian can research an API while a scout maps local call sites. Reviewers can inspect the same files because they are not modifying them.

Writers are different. Two agents editing the same worktree at the same time can invalidate each other's reads, overwrite nearby changes, and produce a result neither one actually tested. Parallel writers require isolated worktrees with disjoint ownership and an explicit integration step.

Most of the time, serialization is cheaper than clever conflict management. I use parallel fan-out for independent evidence gathering, not as a default display of agent abundance.

![A historical capture of oracle and stevedore inspecting review and release concerns in parallel](../../assets/pi-tui-screenshot-3.png)

_A historical capture: parallelism is most useful when ownership is clear. Here, oracle inspects the diff while stevedore checks build, branch, and release state._

## Model policy belongs to the role definition

The lead generally does not override models when dispatching work. Each specialist already has a primary model, ordered fallbacks, a thinking level, and a tool budget chosen for that role.

Scout can use a cheaper route because it returns paths and findings. Artisan gets a model suited to visual judgment. Machinist gets enough turns to implement and validate a concrete unit. Those defaults are part of the configuration, not choices the lead should reopen on every call.

Oracle is the exception. A review is only useful if the reviewer is at least as capable as the orchestrator on the question being judged. That might mean raising the thinking level within the same model family. It might mean switching families to challenge a correlated blind spot. Novelty is not the goal. Independent capability is.

The lead still evaluates the review. Fresh context prevents one class of bias, but it does not make the reviewer automatically correct.

## The custom Apex UI is an orchestration console

The stock terminal was designed around one agent calling tools. Once I had several specialists running, the presentation layer became part of the orchestration system. Streaming every child token into the main session would defeat context isolation, but collapsing a worker to a spinner would hide too much. The Apex UI sits between those extremes.

Its basic unit is a **tool receipt**. A collapsed receipt fits the operational facts on one row: status glyph, tool name, primary argument, optional stats, and duration. A short rail beneath it previews useful output. Expanding the receipt reveals a bounded body without changing how the tool itself executes.

The same receipt engine presents built-in tools such as `read`, `bash`, `edit`, and `write`, plus web-search and MCP calls. That consistency matters when one turn mixes a local file read, an Exa result, a platform API call, and a failed shell command. I can scan one visual grammar instead of decoding a different renderer for every extension.

Edits get special treatment. The expanded result shows a numbered contextual diff with line statistics and intra-line highlights. Writes capture the previous file contents when possible, so replacing a file still produces a real before-and-after diff instead of a generic success message.

Task presentation depends on the execution mode:

- synchronous `task` calls get a rich mission card with the specialist, model, thinking level, turns, activity tree, fallback state, and final report
- async RPC tools return bounded status and result views with lighter worker chrome, because persistent lifecycle control matters more than replaying a full mission card
- pending child dialogs remain visible as checkpoints until the lead answers them with `task_reply`

The footer is a second information layer. It anchors working directory and branch on the left, model and thinking level on the right, and fits token traffic, cache rate, active task count, MCP state, and VS Code state where terminal width allows. Fields degrade by priority as the window narrows instead of wrapping into an unreadable dashboard. The input keeps Pi's editor geometry intact, while the custom border, prompt glyph, and rotating working indicator make the current state easier to locate.

All large output is bounded by lines and characters. Renderers use width-aware fallbacks because a beautiful receipt that crashes on a narrow Windows terminal is not an improvement. Renderer failures go to `pi-render.log` and degrade to short text. `PI_APEX_UI=0` disables the layer entirely if I need to separate an interface bug from an agent or tool failure.

Apex also owns a landing identity: an Observatory splash shown once on a fresh chat, with a shark mark drawn rather than photographed and a workspace-seeded star field whose shape comes from the workspace path and whose density reflects context usage. The shark is there because my son loves sharks, and it felt right to let that show up in something I built. It is a splash, not a status indicator, and its featured roster is currently limited to nine specialists rather than all ten. Compaction still uses Pi's own built-in spinner, and live async workers show through the normal task status cards, not through any shark animation.

The distinction between a receipt and a transcript is the main design decision. I need to know that a worker inspected four files, changed two, hit one failed command, and passed its targeted validation. I usually do not need every intermediate sentence it generated while getting there.

## Repeated workflows became commands and local tools

The configuration has also moved beyond agent dispatch.

`/browser` and `/deploy` are native extension commands now, not just reusable prompt files. They perform deterministic setup before handing control back to the model. The browser command connects to a dedicated debug profile and injects the relevant operating instructions. The deploy command captures worktree facts and constructs a bounded handoff for stevedore.

`/orchestrate` is also native. It toggles a strict orchestrator mode, `on` or `off`, and persists that setting for the session. With it on, the lead loses its inline-by-default allowance: all substantive implementation and broad investigation route to a specialist, and the lead's job narrows to decomposing the request, writing work orders, integrating results, and verifying the outcome. Direct `rg` lookups, lead-run combined validation, and trivial mechanical corrections to a returned diff remain allowed. I reach for it on larger changes where I want to force delegation discipline instead of relying on my own triage in the moment.

`brainstorm` remains a prompt-level mode shift because its job is behavioral: generate divergent options and do not implement until the user chooses a direction.

A handful of local tools round out daily use without deserving their own section.

`todo_write` and `todo_read` keep an explicit session plan. `lsp` gives the lead semantic navigation (definitions, references, symbols, diagnostics) instead of grep-only search. A dedicated PowerShell tool runs Windows-native scripts and services work directly, separate from the portable `bash` default. A read/image result guard blocks redundant re-reads of unchanged images and downscales oversized ones before they burn context.

Long-running shell commands use a separate background-process extension (`bg_start`, `bg_status`, `bg_list`, `bg_kill`) so dev servers and watchers do not hold a foreground tool call open.

External research is provided through local `web_search`, `fetch_content`, and `get_search_content` tools backed by Exa. Fetches are cached so the model can request bounded slices instead of stuffing whole pages into one result.

MCP support is composed locally too. The adapter and Apex's MCP receipt wrapper share the adapter extension's `ExtensionAPI`, so a server's tools get the same receipt chrome as everything else. I don't also load the MCP package independently in the extension list; doing that alongside the local composition would just start a second, inconsistent MCP integration. Two skills, `mcp-scripting` and `mcp-scripting-recipes`, cover the scripting contract and safe, server-agnostic composition patterns for it.

## Implementation and review form a loop

The workflow treats review as part of implementation, not optional polish.

A tiny inline change can get a focused review from the lead. Delegated code, multi-file changes, difficult logic, and security-sensitive work enter a fresh-context review loop:

1. Machinist implements non-visual code, or artisan implements the user-facing interface.
2. Oracle inspects the actual files and diff, not just the worker's summary.
3. If oracle finds a valid issue, the original worker fixes it with the implementation context still available.
4. Oracle reviews the revised diff again.
5. The lead repeats the loop until the findings are resolved, rejected as incorrect, or surfaced as an explicit blocker.
6. The relevant validation runs after the final fixes.

The implementer and reviewer keep different jobs. Machinist or artisan owns the fix. Oracle owns the independent judgment. The lead decides whether a finding is real, speculative, already covered, or outside the requested scope.

![An oracle review finding reporting a type-resolution issue before the lead applies the next fix](../../assets/pi-tui-screenshot-2.png)

_The implementation loop in practice: an oracle review surfaces a concrete finding, and the lead immediately plans the corrective edit before another review pass._

For higher-risk changes, the lead can run additional oracle passes with different strong models. This is not a vote where two reviews automatically beat one. Changing the model is useful when another model family may notice a different failure mode, or when the first review leaves conflicting evidence. Every oracle used for the final gate still needs to be at least as capable as the orchestrating model for the question under review.

The final answer states what passed, what failed, and what was not run.

I also encode delegation and verification gates in the lead prompt. If the lead personally scans half the repository instead of using scout, or implements a substantial multi-file unit instead of assigning the right specialist, it needs a concrete reason. This protects the coordination context from the very behavior the system was built to avoid.

## Crash logs and ignored secrets keep operations boring

A crash-logger extension records uncaught exceptions, unhandled promise rejections, stream errors, and nonzero exits, along with process metadata. A normal, zero-exit shutdown is not logged; the file is a record of failures, not a session log. Environment markers distinguish the main Pi session from a named subagent. The logger is best-effort because a diagnostic hook that causes a second fatal error is worse than no logger.

The configuration repository is portable, but credentials and local runtime state are not part of the portable layer. Authentication, provider configuration, MCP secrets, web-search keys, sessions, run histories, logs, caches, trust state, browser profiles, and continual-memory notes stay ignored.

The agent roster, prompts, extensions, and install metadata belong in git. Secret-bearing and machine-local state do not.

## The tradeoffs got sharper as the system improved

This setup is more capable than the original one-tool version. It also has more ways to fail.

**Two orchestration APIs require judgment.** A synchronous mission is simpler. A persistent worker is easier to steer but requires status handling and cleanup. Picking RPC for every lookup adds ceremony without improving the result.

**Persistent workers can leak capacity.** The async cap counts live workers until `task_close`, even after their current generation settles. The lead has to treat cleanup as part of completing the unit.

**The two caps are independent.** The synchronous runner and async runner each enforce three slots in their own codepath. That is not the same as one global six-worker budget, and I do not treat it as permission to saturate both.

**Fallback behavior is not identical.** The synchronous runner walks its configured fallbacks for failed model attempts and process or result failures, but a task abort, timeout, or turn-limit kill stops the attempt loop rather than triggering another fallback. The async runner only retries a qualifying provider or model failure, and only when replay is still safe: no tool call has started and no visible result has been produced for that generation. Once a worker has done real work, a fallback would risk duplicating it, so the runner leaves the failure attached to the model that produced it instead of replaying the prompt.

**Isolation makes weak prompts fail faster.** A fresh worker does not inherit unstated decisions. Better work orders cost time up front, even though they save time later.

**Parallelism multiplies review work.** Four fast patches still need integration, combined validation, and a coherent final decision. Fan-out moves work around. It does not erase it.

**The extension surface needs testing.** Process control, JSON event parsing, RPC sessions, UI rendering, model routing, and platform-specific termination are real software. Daily use and crash logs catch problems, but they are not a substitute for deeper automated coverage.

## Why this configuration works for me

I built this configuration around the way I already prefer to work. I want one place to discuss the goal, make decisions, and see the final result. I do not want that same context window filled with every repository scan, documentation search, implementation detour, and review pass required to get there.

The specialist roles give recurring jobs a clear owner. Scout gathers local evidence. Librarian handles external research. Machinist or artisan implements. Oracle challenges the result. Stevedore handles the operational finish. I can change the model, tools, and turn budget for each role without rewriting the entire workflow every time.

The two task modes cover the delegation patterns I actually need. A synchronous task is enough for a bounded lookup or review. A persistent RPC worker is better when implementation needs steering, follow-up, or another pass after Oracle finds an issue. The custom UI lets me supervise both without flooding the lead session with worker transcripts.

This is not meant to be a universal agent framework or a claim that every coding task needs a team. I still make small changes inline. The configuration is useful when a job becomes large enough that discovery, implementation, review, and release work start competing for the same context.

The complete setup is available in the [public configuration repository](https://github.com/bskimball/pi). It includes the lead prompt, specialist definitions, extensions, skills, themes, and restore instructions. You can copy the whole setup, but the more useful approach is probably to take the boundaries that match how you work and discard the rest.

---

# Building Compass: A Life Assistant That Still Works When the AI Does Not

> I wanted a more customizable workout tracker, a budget my wife and I could share, and AI help on both. So I built Compass on TanStack Start and Cloudflare, and made sure it still works when the AI does not.

- Author: Brian Kimball
- Published: 2026-07-19
- Tags: ai, cloudflare, privacy-engineering, systems-architecture, reactjs, typescript
- Canonical: https://brian-kimball.com/blog/building-compass-life-assistant/
- Markdown: https://brian-kimball.com/blog/building-compass-life-assistant/index.md

I wanted three things. A workout and nutrition tracker with more customization than the apps I already liked, plus a way to see what workouts AI would generate for me. A way for my wife and me to actually share a budget instead of me maintaining a spreadsheet nobody else opens. And some AI help figuring out where our money was leaking, because staring at a category breakdown once a month was not doing much.

Plenty of workout apps are fine. I still wanted deeper control over the plans, and I was curious whether generated sessions would be any good. They are. Grok even generates exercise reference images for the movements, which is more useful mid-session than I expected.

There was no dramatic breaking point. I sat down one weekend to rebuild the budget spreadsheet and find a nutrition app, and instead of doing either I started building something that did both.

None of the apps I tried did the fitness piece and the household budget piece together the way I wanted, and the ones that did one thing well still left me juggling tools. So I built Compass.

## Why the shape of the app looks the way it does

Those three goals immediately created a problem. My workout history is mine. My wife does not need to see my protein numbers, and I do not need to see hers. But money is not like that. If I hide a purchase from the household budget, the budget is lying.

So from day one Compass had to draw a hard line between personal data and shared data. Health and fitness live in a personal scope tied to whoever is logged in. Finance and some tasks live in a household scope both of us can see and edit. That split is not a nice-to-have I added later. It is the reason the whole storage layer is built the way it is.

The other thing I knew early: I did not want to type. Half the time I want to log a workout or add water intake, I am mid-set or mid-cooking with wet hands. Voice had to be the default way in, not a bonus feature bolted on after the UI shipped.

<figure>
  
  <figcaption>
    The morning dashboard puts the generated workout, daily coaching, weather,
    and voice input on one screen.
  </figcaption>
</figure>

## The stack

Nothing exotic here on purpose:

- **TanStack Start + React + TanStack Router** for the app and its server functions
- **Cloudflare Workers** for deploy and scheduled jobs
- **R2** for domain data, **D1** for Better Auth tables only
- **Better Auth** with a Google allowlist plus passkeys
- **Grok** through a server-side adapter, never from the browser
- **Tailwind + shadcn/ui** for the UI

I picked Cloudflare because I wanted edge deploy with low ops and one obvious place to put secrets. TanStack Start fit because I wanted server functions with real types, not a separate API layer I'd have to keep in sync with the client.

## How requests and storage actually work

Routes stay thin. Route-facing server functions live under `src/server`. The actual domain logic lives in plain `*-impl.ts` modules. Persistence goes through a store interface instead of R2 key strings scattered across the codebase, because I have made that mistake before and cleaning it up later is miserable.

The path a request takes:

1. A route calls a `createServerFn` wrapper.
2. Auth middleware resolves the session and binds the member scope for that request.
3. The wrapper validates input and gates any write.
4. The implementation calls `getDomainStore()` for personal data or `getDomainStore({ shared: true })` for household data.
5. Adapters own the Cloudflare bindings, the SimpleFIN HTTP calls, and the Grok transport.

Domain data lives as objects in R2 with a few conventions: daily aggregates, weekly rollups, reference collections, append-only logs. Most of what Compass does is day-shaped, so that fits better than it should. History is either a weekly rollup or a log, not a sprawling relational schema I'd have to migrate every time the product changed.

Writes to anything contended use compare-and-swap so two people writing at once do not clobber each other. For a household of two that is plenty. I was not going to build a relational migration story before I even knew if the product's shape was right.

I looked at using D1 or Postgres for everything. What I actually needed was cheap object storage for daily JSON, one Cloudflare account for privacy, zero-ops deploys, and a clean wall between auth state and life data. D1 stays auth-only. Nutrition, workouts, finance, chat history, and voice logs never touch it.

The tradeoff is real: R2 is not a query engine. Anything that looks like a complex view gets built in server code from known keys, not SQL. For an app this size that has been fine, and honestly it keeps me from over-designing a schema for data I do not have yet.

## Personal vs household, enforced not assumed

The first version of this app had a single storage prefix, which works fine right up until a second person logs in and can suddenly see your workout log. That was an easy bug to imagine and an easy one to avoid, so I did the scoping properly.

- **Personal:** profile, workouts, nutrition, daily plan, coach chat, voice and AI logs, personal tasks
- **Household:** transactions, budget, subscriptions, category rules, finance snapshots, shared tasks

The middleware resolves the session once per request and binds the scope. Personal stores read that bound scope. Shared stores use the household prefix. If auth is configured and nothing got bound, the store throws instead of quietly defaulting to something. I would rather see an error than accidentally leak data.

Tasks are the one hybrid case, since a task can be personal or shared. The productivity loaders merge both collections for display and split them again on write. Finance is always household, full stop, even on a screen that's showing my personal dashboard.

Passkeys sit on top of the Google allowlist. First sign-in is Google, and after that a passkey works on whatever device I enrolled it on. That matters a lot for a PWA I'm opening half asleep before coffee.

## AI as an upgrade, not a dependency

Every AI call goes through a server adapter. The browser never sees a provider key. Grok handles coaching, chat, voice intent, meal estimation, finance suggestions, workout plan generation, and the exercise reference images.

The rule I actually enforced: every AI path has a deterministic fallback.

- No key configured, the app still generates structural coaching from today's numbers.
- Provider errors or bad JSON come back, it falls back instead of showing a blank screen.
- Chat with no key still streams a useful snapshot of your data and tells you to add a key later.
- Voice with no key falls back to simple text parsing for creating, completing, and deleting tasks, and logging water.

Workouts are where that paid off first for me. Grok builds the weekly plan from preferences, training days, and the rest of the health context, and the sessions have been good enough that I actually follow them. Reference images for each exercise are generated on demand through Grok Imagine, cached in R2, and served from a simple API route so the workout UI can show a silhouette without shipping a giant media library.

Budget optimization follows the same rule. AI advice on where to cut spending is useful when it works. It cannot be the only thing standing between me and a working budget page. Coaching and workout suggestions get generated and saved, so a reload just reads R2 instead of hitting Grok again. Regeneration is something you ask for.

I also didn't hardcode special-case nutrition data just to hide a weak prompt. If food estimation is off, the estimation logic needs to get better. A real curated food database would be its own decision, made on purpose, not a patch to cover for a bad AI response.

## Voice and chat write through the same path

Voice is the main way I interact with this thing day to day. Capture happens in the browser with the Web Speech API, and the transcript gets saved. Intent extraction goes through Grok first for structured JSON and falls back to local parsing if that fails. Additive actions run right away. Anything destructive asks for confirmation first.

Chat handles the longer, open-ended stuff, streaming over a server function instead of a public API route so the same scope middleware protects it. When chat wants to do something voice-shaped, like logging a meal or nudging a task, it produces a proposal card I can apply. Applying it runs through the exact same intent executor the mic uses.

That shared path was one of the better calls I made. Voice and chat could easily have grown two different, slightly inconsistent ways of writing data, and I did not want to maintain both. Other tools in chat are narrower on purpose: memory tools manage coaching context, finance tools can read the ledger, and finance mutations go through their own guarded implementation.

<figure>
  
  <figcaption>
    The midday nutrition panel accepts a meal description, tracks the day's
    targets, and keeps the microphone within reach.
  </figcaption>
</figure>

<figure>
  
  <figcaption>
    Coach can answer questions across personal and household data, then propose
    actions that use the same guarded write path as voice commands.
  </figcaption>
</figure>

## Integrations I'm actually willing to trust

**SimpleFIN Bridge** is the real finance sync. I still support manual CSV import, but balances and transactions from linked accounts now come in through a sealed Access URL stored in household R2. The app never touches a bank password. A Workers cron runs the sync on its own, manual refresh is rate-limited, and everything lands in the same ledger and categorizer as CSV imports, with a cutover date so nothing double-counts.

That's a real privacy tradeoff, since a third party now sees which institutions we're connected to. In exchange, the net worth chart stops depending on whether I remembered to download a statement that month.

**Weather** comes from Open-Meteo through a server function. The client sends coordinates, the server fetches the forecast after auth. No key in the browser.

**Daily quotes** are AI-backed and gated the same way as coaching: nice when Grok is up, not something the app depends on when it isn't.

That's the whole integration list. I'm not going to pad it out.

## Deploying to Cloudflare

Production runs as a Worker. The entry point re-exports TanStack Start's fetch handler and adds a `scheduled` handler for the SimpleFIN cron. Secrets live in Wrangler secrets, non-secret config is a Worker variable. I locked down preview and crawler exposure hard: noindex headers, robots disallow, workers.dev turned off in the deploy config.

Local dev uses the Cloudflare Vite plugin with emulated bindings, and auth degrades gracefully without OAuth set up locally. Production is the opposite. If auth config is missing, it fails closed. I want convenience on my laptop and a locked door on the deployed app, not the reverse.

CI runs format, lint, typecheck, and tests before every build, and deploy goes through the same gate. This thing writes into our actual budget. I did not want it running on the honor system.

## What I traded off, and what's still not done

Compass works. It is not finished, and I'm fine with that.

Things I chose on purpose:

- Object storage over relational convenience, betting the app stays day-shaped
- Browser speech-to-text over the cost and latency of server-side audio, for now
- One coach call that reasons over everything instead of a pile of small model calls
- Shared visibility into household finances over strict isolation, because a half-shared budget defeats the point
- My own SSE chat transport instead of waiting on a clean TanStack AI package for the versions I'm pinned to

What still needs work:

- Household coordination is thin outside of shared tasks and shared money
- The nightly reflection screen is basically a stub sitting on top of data that's already there
- Coach memory is decent but I have no real way to measure if it's actually good yet
- The list of things voice and chat are allowed to do safely is still short
- I might eventually put a real abstraction layer in front of Grok, but only once I'm sure I want to swap providers, not before

The stuff I really can't answer yet is whether any of this changes our actual habits. Does the coaching nudge get ignored after week two? Which voice commands fall apart in a loud kitchen? How often does SimpleFIN need me to go rescue a broken bank connection by hand? I don't have that mileage yet.

## After a few weeks of real use

It has been good, though not evenly good.

The finance side is the clear win. Putting every subscription in one place surfaced a few I did not need and promptly cancelled, which is the kind of thing I had been meaning to audit for years and never did. I also have a much better sense of where the budget actually sits, instead of the vague once-a-month guess I had before. Nutrition is close behind: knowing my real calorie intake, rather than what I assumed it was, changed how I eat more than any coaching prompt did.

The fitness side is the honest miss. The app generates good workouts. I am not doing them as often as I would like. That is not a bug I can fix in the storage layer, and it is worth saying plainly, because the easy version of this post ends with the tool solving everything. Building a thing that tracks a habit turns out to be much easier than building the habit.

So: measurement got better across the board, and one of the three behaviors actually moved. I will take that.

---

# Welcome to the Atomic Drafting Room

> Why I tore down the Renaissance Technical design and rebuilt the site in Raygun Gothic — atomic starbursts, chrome typography, and a turquoise-coral-gold palette that finally feels like me.

- Author: Brian Kimball
- Published: 2026-06-13
- Tags: design, astro, development
- Canonical: https://brian-kimball.com/blog/atomic-drafting-room/
- Markdown: https://brian-kimball.com/blog/atomic-drafting-room/index.md

A few months ago I shipped a redesign I called **Renaissance Technical**. It was a deliberate collision of rigid engineering grids and organic, sketchbook artistry — the idea that Da Vinci never chose between painting and mechanics, so why should a portfolio?

I still love that concept. But after living in it for a while, I realized the palette was too quiet. The parchment textures and restrained earth tones felt safe. I wanted something that hummed with optimism. Something that looked like it was drafted in 1957 by an engineer who genuinely believed the future was going to be incredible.

So I tore it down and started over.

## Enter Raygun Gothic

The new design system is called **Atomic Drafting Room**, and the aesthetic is **Raygun Gothic**: mid-century atomic-age optimism meets the precision of a systems schematic. Think Googie coffeeshop signage, sputnik chandeliers, boomerang Formica tables, and a color palette pulled straight from a 1950s space-race brochure.

I wanted the site to feel less like a museum and more like a **Tomorrowland terminal** — confident, readable, and intentionally colorful. The subject is still the same person (me, a systems engineer), but the packaging now says "building from the metal up" instead of "please browse my gallery."

## What Changed

**Color, everywhere.** The old scheme was cream and charcoal with the occasional accent. The new system runs on three loud, friendly colors: **turquoise**, **coral ray**, and **chrome gold**. Light mode feels like warm print stock under a fluorescent drafting lamp. Dark mode drops you into deep space teal with neon atomic glow. Every page now carries that energy, not just the hero.

**The Atomic Starburst**, a mid-century "sparkle" of thin radiating spikes, is the signature mark. It spins slowly in the hero, anchors the header logo, and punctuates the footer. It is the one place where the design gets to be completely ornamental; everything around it stays disciplined so the starburst can actually sing.

**Typography got a personality transplant.** I dropped the restrained serif and picked up [Audiowide](https://fonts.google.com/specimen/Audiowide) for display — a retro voice that I use sparingly, only for punchy one-word marks. Headings moved to [Outfit](https://fonts.google.com/specimen/Outfit), a geometric sans that reads like a modernist poster. Body copy stayed in Inter for sanity, and technical labels, tags, and dates got [Space Mono](https://fonts.google.com/specimen/Space+Mono) for that data-terminal feel.

**The Googie Skyline** anchors the bottom of every page: a space-age city silhouette at dusk with a domed arch, a needle spire, and a coral banded sun rising behind the content. It is pure atmosphere — a reminder that even the footer gets to have an opinion.

**The Boomerang Divider**, a gradient rule that arcs like an atomic shelf, now separates sections with a turquoise-to-gold-to-coral sweep. And the **Border Comet** — a gold tracer that races around Bento cards on hover — adds a little 1950s dashboard energy to the interactive moments.

## The Philosophy: Bold in One Place

The most important rule I wrote down was this: spend your boldness in **one** place. If the starburst is screaming, the surrounding grid whispers. If the footer skyline is a full-color sunset, the body text stays neutral and highly legible. The conflict is between atomic, optimistic ornament and the disciplined geometry of a technical document. That tension is the whole point.

I also removed the FIG annotation system from the previous design — the monospaced `FIG 1.0 //` labels that dotted every page. They were clever, but they added visual noise without adding meaning. The new system trusts color and shape to do the communicating.

## Why This Feels Right

I have spent most of my career in the infrastructure layer — IBM i systems, network discovery tools, automation scripts, bare-metal servers. That work is invisible by default. This design makes the invisible feel **visible and exciting**. It is not trying to be a SaaS landing page. It is trying to be a personal expression of someone who genuinely loves the craft of building systems.

The stack underneath is still Astro, React islands, Tailwind v4, and shadcn/ui. The surface changed; the foundation did not. That felt appropriate. Raygun Gothic is a coat of paint, but it is a very intentional coat of paint.

If you are reading this, the facelift is live. Toggle the light/dark switch in the header and watch the starburst spin. I hope it feels like the future we were promised.

---

# Bringing AI to IBM i: An MCP Server with 31 System Tools

> A BYOK MCP server that exposes 31 facade tools for IBM i observability, job management, and security. Built to train new users and accelerate expert workflows.

- Author: Brian Kimball
- Published: 2026-05-22
- Tags: typescript, development, ibmi, ai, mcp
- Canonical: https://brian-kimball.com/blog/ibmi-ai-mcp/
- Markdown: https://brian-kimball.com/blog/ibmi-ai-mcp/index.md

[NPM Package](https://www.npmjs.com/package/@bdkinc/ibmi-mcp)

## Preface

This project started from a simple need: **training new IBM i users**. The platform is powerful but complex, and getting newcomers up to speed takes time. I wanted to build something that could act as an intelligent assistant—one that knows the system internals and can guide users through operations, diagnostics, and discovery.

That idea evolved into a full toolkit. We are currently testing it internally against live IBM i servers. It is still in beta, but the shape of it is solid enough to share.

## What It Is

At the center is an **MCP (Model Context Protocol) server** published as [`@bdkinc/ibmi-mcp`](https://www.npmjs.com/package/@bdkinc/ibmi-mcp). It exposes deep system integration to any AI that speaks MCP. Because it is **BYOK** (Bring Your Own AI), you provide the model and the server provides the tools.

Surrounding the server are two interfaces:

- **A web application** that connects directly to the MCP server.
- **A desktop application** for users who prefer a native client.

Both are built to make IBM i feel less like a black box and more like a conversation.

## Architecture

The server is built on **Hono** and talks to IBM i through [**knex-ibmi**](https://www.npmjs.com/package/@bdkinc/knex-ibmi) and **nodejs-itoolkit**. The web app itself is built on top of my [**tanstack-hono**](https://github.com/bskimball/tanstack-hono) SSR monolith template.

Everything lives in a single **monorepo** with a shared design system, which keeps the desktop and web experiences consistent.

## Shrinking 184 Tools Down to 31

An early version of the server exposed **184 individual tools**. The sheer volume created two concrete problems.

**First**, VS Code (and several other MCP clients) enforces a hard limit of **128 tools**. We were well beyond that.

**Second**, 184 options made LLM decision-making measurably harder. Too much surface area led to hesitation and incorrect selections.

I redesigned the tool surface down to **31 core facade tools** that cascade into multiple selectors. Each facade tool accepts a `view` or `operation` parameter, so the LLM makes a coarse decision first, then drills down. This also lets a single MCP session connect to **multiple IBM i servers** without tool duplication. The result is a cleaner interface, faster model decisions, and a more intuitive path from intent to action. We aimed for a mix where the user and agent both have freedom to explore, while guided workflows keep actions predictable and safe.

### The 31 Core Tools

The tools are grouped into **Observe** (read-only), **Recommend & Act** (mutating), and **General & Context** helpers.

#### Observe (Read-Only Facades)

| Tool | Purpose |
|------|---------|
| `inspect_system` | Hardware health, OS identity, PTF status, disk, memory pools, system values |
| `inspect_storage` | Storage pools, consumption, temp usage by job, ASP configuration |
| `inspect_jobs` | Active, scheduled, and queued jobs, job queues, performance pressure |
| `inspect_subsystems` | Running subsystems and subsystem descriptions |
| `inspect_spool` | Output queues, spooled files, printer writers, file contents |
| `inspect_messages` | History log, QSYSOPR, job logs, message queues |
| `inspect_ifs` | IFS directories, file content, search, metadata |
| `inspect_objects` | Library objects, locks, save history, authority |
| `inspect_network` | TCP servers, listeners, connections, HTTP servers, NetServer, DRDA |
| `inspect_users` | User profiles, authorities, stale accounts, password expiration |
| `inspect_security` | Audit config, certificate expiry, login activity, authorization lists |
| `inspect_database` | Db2 schemas, tables, indexes, foreign keys, SQL services |
| `inspect_powerha` | High-availability clusters, CRGs, replication, HyperSwap |
| `inspect_brms` | Backup history, control groups, media, job activity |
| `run_sql_recipe` | Execute audited, read-only IBM i diagnostic SQL recipes |

#### Recommend & Act (Mutating Facades)

All state-changing tools use a two-phase **preview → approve → execute** lifecycle.

| Tool | Purpose |
|------|---------|
| `manage_job` | End, hold, release, or change job properties |
| `manage_job_queue` | Hold or release job queues |
| `manage_subsystem` | Start or end subsystem descriptions |
| `manage_spool_file` | Delete, hold, or move spooled files |
| `manage_ifs_path` | Delete, rename, move, create/remove directories, change IFS authority |
| `manage_ifs_backup` | Save or restore IFS directories |
| `manage_tcp_server` | Start or stop TCP network daemons |
| `manage_messages` | Reply, send, or clear messages |
| `manage_user_profile` | Create, disable, reset password, change, or delete user profiles |
| `manage_object_security` | Grant/revoke authority, change owner, restore libraries |

#### General & Context

| Tool | Purpose |
|------|---------|
| `plan_run_ibmi_command` | Preview any CL command and generate an approval context |
| `run_ibmi_command` | Execute an arbitrary CL command after approved planning |
| `manage_session` | Set or retrieve session settings like library lists (`*LIBL`) |

## What You Can Do With It

For **new users**, the assistant is a tutor. It can walk you through navigating the IFS, checking active jobs, or understanding spool files.

For **experienced operators**, it is an accelerator. You can ask for quick storage analysis, filtered job logs, or targeted health checks. Instead of memorizing command syntax, you describe what you need and the server translates that into the right system calls.

## The Apps in Action

## Current Status

The project is in **beta** and is being validated internally on production IBM i environments. The goal is to harden the toolset, refine the selectors, and ensure the responses are reliable before a wider release.

If you work with IBM i and are curious about augmenting it with AI, the MCP server is available on [npm](https://www.npmjs.com/package/@bdkinc/ibmi-mcp). Install it, bring your own model, and see what the system can tell you.

---

# Surveyor: A Field-Tested Network Discovery Tool for MSPs

> From Raspberry Pi prototype to Wails desktop app: how Surveyor replaces manual nmap commands with stealth scanning and RFC 1918 auto-discovery.

- Author: Brian Kimball
- Published: 2026-01-19
- Tags: development, golang, reactjs, networking
- Canonical: https://brian-kimball.com/blog/surveyor-msp/
- Markdown: https://brian-kimball.com/blog/surveyor-msp/index.md

[Download and Instructions](https://surveyor-msp.com)

## Evolution

This project is the spiritual and technical successor to the [Network Mapper](/blog/network-mapper/) tool I started a couple of years ago. It has gone through several iterations—briefly known as "Gatherer"—before maturing into **Surveyor**.

While the original goal remains the same—simplifying network discovery for Managed Service Providers (MSPs)—this version is a complete reimagining based on real-world usage and field techniques.

## Field-Tested Discovery

Surveyor is designed to be the first tool a technician runs when stepping onto a new site. It replaces the need to manually memorize and execute `nmap` flags or `nslookup` commands, streamlining the onboarding process.

Key capabilities include:

- **Stealth Scanning:** Utilizes techniques to evade firewalls and gather data without triggering immediate alarms.
- **RFC 1918 Discovery:** Automatically scans all local private network ranges to find devices across subnets.
- **Network Discovery:** Identifies other networks and potential pivots.

The tool is actively used by the MSP team at my current company, ensuring that features are driven by actual field requirements rather than just theory.

## Technical Architecture

We made significant changes to the stack to leverage a more robust ecosystem and improve maintainability.

- **Backend:** We stuck with **Wails** but rewrote the core network tools in **Go**. We still rely on the battle-tested **nmap** for the heavy lifting of scanning.
- **Frontend:** We migrated from SolidJS to **React**. This allows us to tap into a wider ecosystem, specifically using **React Aria Components** and **Tailwind CSS** for our design system.
- **Monorepo:** The project is now structured as a monorepo. This configuration allows us to share the design system across the application and the marketing site, and automatically updates the website version when we build the app.

## AI-Assisted Development

This project reached the finish line with significant help from AI agents.

- **Design:** We utilized **Gemini Pro** with a specialized frontend-design skill to architect the design system.
- **Backend:** **OpenAI's GPT Codex** was instrumental in writing much of the Go backend logic.

Surveyor represents a modern approach to tool building: combining battle-hardened binaries like nmap with modern UI frameworks and AI-accelerated development.

You can find the download and field guide at [surveyor-msp.com](https://surveyor-msp.com).

---

# Renaissance Technical: Designing with AI

> How I used OpenCode and Gemini to build a "Renaissance Technical" design system that pits rigid grids against organic circles—and why AI is the brush, not the painter.

- Author: Brian Kimball
- Published: 2026-01-04
- Tags: design, ai, astro, development
- Canonical: https://brian-kimball.com/blog/renaissance-technical/
- Markdown: https://brian-kimball.com/blog/renaissance-technical/index.md

I recently pushed a major update to this website's design system, dubbing it **"Renaissance Technical."**

While I enjoyed my previous design, I felt the itch to switch things up and explore a new creative direction. I wanted to build something that felt less like a standard portfolio and more like a personal expression of my background.

In college, I studied Art, and I was always drawn to the **Renaissance** period. There was no distinction then between the artist and the engineer. Da Vinci didn't choose between painting and mechanics; he did both. I wanted a design that reflected that duality.

## The Inspiration

I was heavily inspired by the work at [AmpCode](https://ampcode.com), specifically their fearless use of bold colors and whimsical artwork. They proved that technical content doesn't have to look "technical."

I wanted to move away from the outer space aesthetic and shooting star motifs of my previous design. I wanted something that felt like a **sketchbook**—a place where the organic (art) crashes into the engineered (code).

## The Philosophy: Da Vinci's Notebook

The core aesthetic is **"Renaissance Technical"**. It sits at the intersection of the biological and the mechanical.

- **The Grid (The Engineer):** Rigid layouts, schematic lines, and monospaced "FIG" annotations. This represents the structure of code.
- **The Soul (The Artist):** Serif typography (`Tenor Sans`), organic circles, and deep, parchment-like textures. This represents the human element.

It’s a visual conflict between the "Square" (logic) and the "Circle" (emotion).

## The Workflow: AI as Creative Partner

This redesign was an experiment in a new workflow using **OpenCode** and the **Gemini** family of models. I've found Gemini to be exceptionally strong in creativity, making it my go-to choice for writing, design, and coding.

### 1. Ideation (Gemini Pro)

I start by working with **Gemini Pro** to iterate on ideas. I often use prompts like, _"As a group of designers, let's explore ideas to..."_ allowing us to brainstorm without writing a single line of code.

We explore different vibes, get feedback, and refine concepts purely at the abstract level. This creates a sandbox where I can act as the Creative Director rather than the implementer.

### 2. The Plan (Gemini Pro)

Once we settle on a direction (in this case, "Renaissance Technical"), I use Gemini Pro to formalize it into a plan. The goal is to create concise, actionable tasks that can be executed in parallel. This turns a vague creative vision into a structured engineering roadmap.

### 3. Execution (Gemini Flash)

The actual building is handled by subagents powered by **Gemini Flash**. These agents take the tasks from the plan and execute them. Flash is incredibly fast and accurate—scoring highly on SWE-bench benchmarks—which allows it to handle the bulk of the implementation work efficiently while I maintain high-level oversight.

## The Human in the Loop

This process reaffirmed something important: **AI is the brush, not the painter.**

The AI could generate the grid, but it couldn't decide _why_ the grid mattered. It couldn't feel the nostalgia for my college art classes. It couldn't make the decision to embrace the "Renaissance" aesthetic over a safer, more modern choice.

I acted as the conductor; the AI was the orchestra. And together, we brought this new vision to life, creating a design that I'm truly excited to share.

---

# Designing Fully Redundant Infrastructure: Physical and Logical Layers Explained

> How redundant compute, SAN mirroring, load balancers with CARP, and synchronous database clustering eliminate both hardware failures and maintenance windows.

- Author: Brian Kimball
- Published: 2025-11-30
- Tags: networking, servers
- Canonical: https://brian-kimball.com/blog/high-availability/
- Markdown: https://brian-kimball.com/blog/high-availability/index.md

High availability (HA) ensures systems remain operational despite component failures. By implementing redundancy at both physical and logical layers, organizations can minimize downtime and enable seamless maintenance.

This guide outlines a fully redundant architecture designed for critical enterprise workloads.

## The Physical Layer

To ensure full redundancy, every hardware component is deployed with a companion device. Interconnections utilize distinct cabling standards for specific traffic types:

- **Network Traffic:** Blue lines represent standard TCP/IP communications (CAT5e or better) for management and user access.
- **Storage Traffic:** Orange lines represent Fiber Channel connections for high-speed, reliable data transmission.
- **Power:** Each device connects to independent power sources to prevent single-point electrical failures.

### Compute Nodes

Compute nodes (hosts) run the hypervisor (e.g., vSphere) on mirrored internal drives.

- **Virtualization:** Hosts form a cluster where HA software automatically migrates and restarts virtual machines on healthy nodes if a hardware failure occurs.
- **Capacity:** The cluster must have sufficient reserve capacity to handle the workload of a failed node without performance degradation.

### Storage Area Network (SAN)

Storage redundancy is critical. While software-defined storage (vSAN) is an option, physical SANs offer robust hardware-level replication.

- **Mirroring:** SANs utilize policy-based mirroring to replicate data across storage units in real-time.
- **Connectivity:** Compute nodes connect to SAN switches via multipath Fiber Channel. Zoning on switches ensures secure and redundant paths between hosts and storage arrays.

## The Logical Layer

The logical layer defines how data and requests flow through the infrastructure. This design decouples the service availability from the underlying operating system state, allowing for zero-downtime maintenance.

### Load Balancing & Proxies

All incoming requests hit the load balancer or proxy layer first.

- **Virtual IP (VIP):** Technologies like pfSense use CARP to present a single virtual IP address to clients.
- **Failover:** If the primary load balancer fails, the backup assumes the VIP instantly.
- **Distribution:** Traffic is distributed to application servers based on health checks and load metrics.

### Application Servers

Application servers (e.g., IIS) process the business logic.

- **Statelessness:** ideally, these servers store no unique local data.
- **Shared Data:** Any persistent content should reside on shared network storage or the database.
- **Updates:** Administrators can patch and reboot Server A while Server B handles all traffic, then repeat for Server B.

### Database Clusters

Data integrity requires stricter synchronization than application code.

- **Clustering:** MSSQL Clustering (or similar technologies) ensures transactional consistency between database nodes.
- **Sync:** Data is synchronously replicated to ensure the secondary node has an up-to-date copy at all times.

## Summary

This multi-layered approach to redundancy achieves two primary goals:

1.  **Resilience:** Hardware failures (switches, cables, servers, storage) do not interrupt service.
2.  **Maintenance:** Systems can be patched and updated during business hours without downtime.

**Note:** High availability is not a backup strategy. While it protects against hardware failure, it does not prevent data corruption or ransomware. A separate, immutable, off-site backup strategy remains essential for disaster recovery.

---

# A Production-Ready Knex Dialect for IBM i DB2

> Streaming queries, emulated RETURNING, multi-row inserts, and a custom migration runner. This is the Knex dialect I wish existed when I started building on IBM i.

- Author: Brian Kimball
- Published: 2025-09-20
- Tags: typescript, development, ibmi
- Canonical: https://brian-kimball.com/blog/knex-ibmi/
- Markdown: https://brian-kimball.com/blog/knex-ibmi/index.md

[Github Repo](https://github.com/bdkinc/knex-ibmi)

[NPM Package](https://www.npmjs.com/package/@bdkinc/knex-ibmi)

When building Node.js applications on IBM i, I traditionally used the low-level [node-odbc](https://github.com/IBM/node-odbc) driver. While functional, I prefer the expressiveness of a query builder or ORM. Knex is a popular, lightweight query builder used by frameworks like [Feathers](https://feathersjs.com/).

This project implements a first-class Knex dialect for IBM i DB2 over ODBC. Starting as a fork of the unmaintained [knex-db2](https://github.com/henryjw/knex-db2), it evolved into a ground-up TypeScript rewrite with modern packaging (ESM + CJS) and IBM i-specific behavior.

The current version includes a custom migration runner, multi-row insert strategies, `RETURNING` emulation, streaming with adaptive fetch sizing, and improved error handling.

## Quick Start

```ts
import { knex } from 'knex'
import { DB2Dialect, DB2Config } from '@bdkinc/knex-ibmi'

const config: DB2Config = {
  client: DB2Dialect,
  connection: {
    database: '*LOCAL',
    host: '127.0.0.1',
    port: 8471,
    user: 'user',
    password: 'password',
    driver: 'IBM i Access ODBC Driver',
    connectionStringParams: {
      DBQ: 'MYLIB',
      CMT: 0,
      NAM: 1,
      ALLOWPROCCALLS: 1,
    },
  },
  pool: { min: 2, max: 10 },
  ibmi: { multiRowInsert: 'auto' },
}

export const db = knex(config)
```

---

## Feature Highlights

- **Knex 3 Integration:** Fully integrated query building and execution.
- **Transactions:** Includes a custom transaction class.
- **Streaming:** Adaptive streaming (`.stream({ fetchSize })`) with cursor and async iteration support.
- **Multi-row Inserts:** Strategies include `auto`, `sequential`, and `disabled`.
- **Emulated Returning:** Support for `INSERT`, `UPDATE`, and `DELETE`.
- **Identity Retrieval:** Sequential retrieval using `IDENTITY_VAL_LOCAL()`.
- **Migration Runner:** Custom runner to bypass fragile Knex locking on DB2.
- **Developer Experience:** Fully typed (TypeScript), lenient identifier handling, and enhanced error classification.

---

## Multi‑Row Insert Strategies

Configure via the `ibmi.multiRowInsert` option:

```ts
const db = knex({
  client: DB2Dialect,
  connection: {
    /* ... */
  },
  ibmi: { multiRowInsert: 'auto' }, // 'auto' | 'sequential' | 'disabled'
})
```

- `auto`: Single `INSERT` with multiple `VALUES`. Returns all rows when safe.
- `sequential`: Inserts rows individually, capturing identity each time.
- `disabled`: Backwards compatible single-row behavior.

Use `sequential` for deterministic identity values per row, and `auto` for maximum throughput.

---

## Emulated Returning

Since ODBC lacks native `RETURNING` support, this dialect emulates it:

- **INSERT (auto):** Wraps insert to surface inserted rows.
- **INSERT (sequential):** Per-row insert + `IDENTITY_VAL_LOCAL()`.
- **UPDATE:** Executes update, then re-selects affected rows using the original `WHERE` clause.
- **DELETE:** Selects rows first, then deletes them, returning the data.

Select only necessary columns (`.returning(['ID','STATUS'])`) to reduce overhead.

---

## Streaming

Consume large result sets via a cursor stream with adaptive fetch sizing:

```ts
const stream = await db('LARGETABLE').select('*').stream({ fetchSize: 200 })
for await (const row of stream) {
  // process row
}
```

Complex queries (joins, aggregates) automatically increase fetch size, which you can also override manually.

---

## Migration Runner (IBM i Specific)

Standard Knex migrations often fail on IBM i due to auto-commit DDL and locking. This dialect includes a purpose-built runner:

```ts
import { createIBMiMigrationRunner } from '@bdkinc/knex-ibmi'
const runner = createIBMiMigrationRunner(db, {
  directory: './migrations',
  tableName: 'KNEX_MIGRATIONS',
  schemaName: 'MYSCHEMA',
})
await runner.latest()
```

CLI usage:

```bash
npx ibmi-migrations migrate:latest
npx ibmi-migrations migrate:rollback
npx ibmi-migrations migrate:status
```

Supports JS, TS, MJS, and CJS migration files.

---

## Error Handling & Debugging

Errors are classified by type (connection, timeout, SQL). Set `DEBUG=true` for concise output, including timing and statement diagnostics.

---

## Current Maturity

This library is production-ready and stable. Future improvements include richer diagnostic logging and tuning for large batch sequential inserts.

If you hit an edge case, please open an issue—real-world feedback helps solidify the API.

---

## Closing Thoughts

Knex becomes a compelling option on IBM i with correct streaming, transactions, returning emulation, and safe migrations. This dialect provides those building blocks without enforcing heavy abstractions.

Give it a spin and let me know what you build with it.

---

# TanStack Hono: A Lightweight SSR Monolith Without the Meta-Framework Weight

> Single-process SSR + CSR with type-safe RPC, Vite, React, and Hono. The goal is low operational complexity without giving up dynamic UI.

- Author: Brian Kimball
- Published: 2025-09-20
- Tags: typescript, development, reactjs
- Canonical: https://brian-kimball.com/blog/tanstack-hono/
- Markdown: https://brian-kimball.com/blog/tanstack-hono/index.md

## Preface

I've been using TanStack Router and Hono for a while and appreciate their shared philosophy: TanStack Router offers a strongly typed, data-aware routing core, while Hono provides a tiny, fast, standards-based HTTP layer.

This project explores combining them into a single TypeScript SSR + CSR monolith. It runs as one process in development, handling server-rendered React routes, client hydration, and lightweight RPC endpoints without the weight of a full meta-framework.

I adapted the official TanStack Router SSR example, swapping Express for Hono and adding a typed RPC API.

Repo: [github.com/bskimball/tanstack-hono](https://github.com/bskimball/tanstack-hono)

Development relies on the Hono dev server with Vite, ensuring hot reloading works across routes, server handlers, and shared types.

## Why Hono?

Hono is a minimalist, high-performance web framework. Its small footprint makes it ideal for serverless and edge environments. Its middleware system makes composing request handlers, RPCs, Zod validations, and OpenAPI-compatible REST APIs straightforward.

## Why TanStack Router?

TanStack Router is a powerful routing library with first-class type safety and data awareness. It supports nested routes, route loaders, and seamless React integration. Its type safety catches errors at compile time, and its flexibility allows it to run in various environments.

## Why a "Monolith"?

Here, "Monolith" means a single repository and runtime process handling:

- HTTP routing (API + SSR HTML).
- React server rendering + hydration.
- Static asset compilation (Vite).
- Type-safe internal RPC endpoints.

The goal: low operational complexity with progressive enhancement and dynamic UI.

---

## Architecture Overview

1.  **Request enters Hono.**
2.  **API/RPC Check:** Matches against routes like `/api/todos`. If matched, returns JSON.
3.  **SSR Fallback:**
    - Creates a TanStack Router instance.
    - Resolves route elements (no framework-specific loaders).
    - Renders React to string/stream.
    - Injects serialized router state and asset tags.
4.  **Hydration:** Client hydrates the router; subsequent navigation is client-side.

---

## File Layout

```
src/
  components/      # Shared React components
  routes/          # TanStack Router file-routes
    __root.tsx
    index.tsx
    about.tsx
    todos/
  api/             # Hono handlers / RPC
    todos.ts
  shared/          # Shared Types / Zod schemas
    types.ts
  entry-client.tsx # Hydration entry
  entry-server.tsx # Server render entry
```

If you want a leaner alternative to Next.js but still need SSR, type-safe RPC, and file-based routing, this stack is worth exploring. The repo is a complete working example you can clone and extend.

---

# Building a Type-Safe Full-Stack Starter with Fastify and Astro

> A complete starter template combining Astro, Fastify, tRPC, Lucia, React, NextUI, and Drizzle into a single type-safe monolith.

- Author: Brian Kimball
- Published: 2024-07-25
- Tags: development, fastify, astro, typescript
- Canonical: https://brian-kimball.com/blog/fastify-astro-starter/
- Markdown: https://brian-kimball.com/blog/fastify-astro-starter/index.md

[Github Repo](https://github.com/bskimball/astro-fastify-starter)

### Latest Update

Following the deprecation of [Lucia](https://github.com/lucia-auth/lucia/discussions/1707), I migrated session management to the recommended approach at [lucia-next.pages.dev](https://lucia-next.pages.dev/). This update also includes general fixes and improvements, such as resolving dark mode screen flashes.

## Why This Stack?

As a solo developer, I often build JavaScript applications with separated server and client builds. While this is a valid approach, managing state across both can be complex.

I missed the simplicity of server-generated content enriching the client. Meta-frameworks like Next.js, Remix, Nuxt, and Astro handle this with SSR, but I wanted a solution that combined Astro's frontend capabilities with a robust backend like Fastify.

## The Two Integration Attempts

I explored two integration approaches:

1. **Astro Adapter for Fastify:** I found a [repository](https://github.com/matthewp/astro-fastify) with contributions from both Astro and Fastify maintainers. However, it didn't work out of the box. Although I [forked it](https://github.com/matthewp/astro-fastify) and fixed dev mode, production mode remained problematic.

2. **Node Middleware:** Astro's documentation provides [middleware instructions](https://docs.astro.build/en/guides/integrations-guide/node/#middleware) for Node adapters. While this offers a clean separation, the example server wasn't in TypeScript. Since I planned to use tRPC and Drizzle, a non-TypeScript backend was a dealbreaker.

Ultimately, I realized I could update my npm scripts to use `tsx` for watching and `rollup` for building. This flexible configuration aligns with Vite's tooling and the recommended Astro setup.

## What's Included

This starter combines Astro, Fastify, tRPC, Lucia, React, NextUI, and Drizzle. It features:

- Full TypeScript support for end-to-end type safety.
- Session-based authentication via Lucia.
- Pre-configured login and registration flows.

Check out the [Github Repo](https://github.com/bskimball/astro-fastify-starter) and let me know your thoughts.

---

# Life Update

> Marriage, a new baby, and a pivot from Raspberry Pi tools to desktop apps. Here is what I have been building during the quiet months.

- Author: Brian Kimball
- Published: 2024-06-01
- Tags: life
- Canonical: https://brian-kimball.com/blog/personal-update/
- Markdown: https://brian-kimball.com/blog/personal-update/index.md

So, I've been pretty busy lately. I haven't written any technical content, but I have some things I am working on. My last blog post was in January about working with Go (golang). Since then, I married the love of my life in February. Then in May we welcomed our baby boy into the world. As you can imagine, we have been a little busy.

### What am I working on?

**Gatherer**

I'm developing **Gatherer** (the precursor to [Surveyor](/blog/surveyor-msp/)). I experimented with a Raspberry Pi form factor early on, but the workflow did not fit how technicians operate in the field. I've since pivoted to a standalone desktop app for MSPs.

- **Stack:** [Wails](https://wails.io) (Go + Vite + React).
- **Why:** Go's standard library handles TCP/UDP communications more effectively than Node.js.

  I'll share a deep dive into the tech stack in a future post.

**Time Tracker**
Once Gatherer enters testing, I plan to revisit my [Time Tracker App](/blog/time-tracker/). Built with [Laravel](https://laravel.com), this app simplifies time entry for warehouse and fulfillment center staff.

**Certifications**
After 14 years in networking, I intend to formalize my experience with Cisco certifications.

Thanks for reading, I hope to have some more technical content available soon!

---

# Go Is the Right Choice for Network Tools. Here Is the Proof

> Six months into Go, I rewrote a network scanner and built a custom event bus. The language is simpler than expected, with one big limitation on IBM i.

- Author: Brian Kimball
- Published: 2024-01-20
- Tags: development, golang
- Canonical: https://brian-kimball.com/blog/learning-go-pt2/
- Markdown: https://brian-kimball.com/blog/learning-go-pt2/index.md

## Six Months In: Lessons Learned

I really like Go. It's easy to pick up, the standard library is comprehensive, and the syntax is intentionally simple. I've been rewriting my network-mapper app, and Go is the right choice for the server.

Unfortunately, Go (and Rust) does not run natively on IBM i. Despite the AIX-based PASE environment, issues with `mmap` and the IFS prevent execution (see this [GitHub issue](https://github.com/golang/go/issues/45017)). For now, I'll use Go in Linux environments and stick to Node or Python on IBM i.

### The App: Gatherer

I renamed the app to **Gatherer**. It's designed for quick network and MSP client evaluations.

**Architecture**

- **Server:** [Echo framework](https://echo.labstack.com/) (Go).
- **Background Tasks:** Uses tools like `nmap` and `whois`.
- **Real-time:** Event-driven architecture with WebSockets.
- **Database:** GORM (ORM) for easy filtering.
- **Pub/Sub:** Custom implementation using Go channels.

**Frontend**

I initially tried Solid.js but reverted to React due to ecosystem maturity and IDE support.

- **Stack:** React, React Query, React Router, React Hook Form.
- **UI:** [react-aria-components](https://react-spectrum.adobe.com/react-aria/index.html) + [daisyUI](https://daisyui.com/). This combination offers accessible components with clean CSS.

### Event Bus Implementation

Here is the custom Event Bus I implemented, inspired by the Revel framework. It supports separate "rooms" for broadcasting and subscribing.

```go
package events

import (
	"container/list"
	"time"
)

var rooms = make(map[string]Room)

type Room struct {
	Name        string `json:"name"`
	subscribe   chan (chan<- Subscription)
	unsubscribe chan (<-chan Event)
	publish     chan Event
	Subscription
}

func NewRoom(name string) Room {
	r, ok := rooms[name]

	if ok {
		return r
	}

	var (
		subscribe   = make(chan (chan<- Subscription), archiveSize)
		unsubscribe = make(chan (<-chan Event), archiveSize)
		publish     = make(chan Event, archiveSize)
	)
	room := Room{Name: name, subscribe: subscribe, unsubscribe: unsubscribe, publish: publish}
	room.init()
	rooms[name] = room
	return rooms[name]
}

func (r *Room) drain(ch <-chan Event) {
	for {
		select {
		case _, ok := <-ch:
			if !ok {
				return
			}
		default:
			return
		}
	}
}

func (r *Room) Cancel() {
	r.unsubscribe <- r.Subscription.New
	r.drain(r.Subscription.New)
}

func (r *Room) Broadcast(typ string, data interface{}) {
	r.publish <- Event{Type: typ, User: "go", Timestamp: int(time.Now().Unix()), Data: data}
}

func (r *Room) Subscribe() Subscription {
	resp := make(chan Subscription)
	r.subscribe <- resp
	return <-resp
}

func (r *Room) work() {
	archive := list.New()
	subscribers := list.New()

	for {
		select {
		case ch := <-r.subscribe:
			var events []Event
			for e := archive.Front(); e != nil; e = e.Next() {
				events = append(events, e.Value.(Event))
			}
			subscriber := make(chan Event, archiveSize)
			subscribers.PushBack(subscriber)
			ch <- Subscription{events, subscriber}

		case event := <-r.publish:
			for ch := subscribers.Front(); ch != nil; ch = ch.Next() {
				ch.Value.(chan Event) <- event
			}
			if archive.Len() >= archiveSize {
				archive.Remove(archive.Front())
			}
			archive.PushBack(event)

		case unsub := <-r.unsubscribe:
			for ch := subscribers.Front(); ch != nil; ch = ch.Next() {
				if ch.Value.(chan Event) == unsub {
					subscribers.Remove(ch)
					break
				}
			}
		}
	}
}

func (r *Room) init() {
	go r.work()
}
```

The event bus works well enough for my use case, though I would reach for a proper message queue like NATS if the traffic grew significantly. If you spot a bug or have a cleaner pattern for Go channel-based pub/sub, I am interested.

---

# Why I Picked Up Go After 20 Years of Web Development

> After two decades of JavaScript, PHP, and Python, I am picking up Go to solve a real performance problem—and the early impressions are strong.

- Author: Brian Kimball
- Published: 2023-11-16
- Tags: development, golang
- Canonical: https://brian-kimball.com/blog/learning-go/
- Markdown: https://brian-kimball.com/blog/learning-go/index.md

## Preface

I've been a web developer for roughly 20 years, starting with Macromedia Flash and Dreamweaver. My journey began with PHP and jQuery, moving through Ruby, Python, Node, and Elixir. While JavaScript/TypeScript is my favorite for its syntax and ecosystem, I know it's not always the best tool.

Recently, I've seen Go (Golang) heralded for its performance and Developer Experience (DX).

## Why Go?

<figure>
  <blockquote>
    Go was designed at Google in 2007 to improve programming productivity in an
    era of multicore, networked machines and large codebases.
  </blockquote>
  <figcaption>- Wikipedia</figcaption>
</figure>

Designed partially out of frustration with C++, Go offers a robust standard library, static typing with excellent IDE support, and no VM requirement. It excels in networking and CLI applications.

## Why Go?

<figure>
  <blockquote>
    Go was designed at Google in 2007 to improve programming productivity in an
    era of multicore, networked machines and large codebases.
  </blockquote>
  <figcaption>- Wikipedia</figcaption>
</figure>

Designed partially out of frustration with C++, Go offers a robust standard library, static typing with excellent IDE support, and no VM requirement. It excels in networking and CLI applications.

## The Plan: Rebuilding Network Mapper

After completing the website walkthrough, I found Go's syntax readable and intuitive. To accelerate my learning, I used this [GitHub repo](https://github.com/miguelmota/golang-for-nodejs-developers) which highlights syntax differences for Node.js developers.

I plan to rewrite my [network-mapper](/blog/network-mapper/) app in Go. The original JavaScript application (Fastify + Solid.js) struggled with performance during `nmap` scans. Go should resolve these issues.

- **Frontend:** Solid.js (switching to Kobalte for accessible components).
- **Database:** Switching from PostgreSQL to SQLite for portability.
- **Backend:** [Echo framework](https://echo.labstack.com/).
- **Deployment:** Embedded Vite SPA within a single Go binary (using [this guide](https://dev.to/aryaprakasa/serving-single-page-application-in-a-single-binary-file-with-go-12ij)).

I will share the GitHub repo once I make more progress. If you are a Node developer curious about Go, the [golang-for-nodejs-developers](https://github.com/miguelmota/golang-for-nodejs-developers) cheatsheet is the fastest on-ramp I have found.

---

# Stop Using Arrays for Real-Time State in React

> Using a native Map instead of an array eliminates O(n) scans for updates and simplifies real-time event handlers in React.

- Author: Brian Kimball
- Published: 2023-11-06
- Tags: development, reactjs, javascript
- Canonical: https://brian-kimball.com/blog/react-state-map/
- Markdown: https://brian-kimball.com/blog/react-state-map/index.md

## The Problem with Arrays

For years, I stored fetched data in arrays—whether using React state, React Query, or SWR. While fetching libraries like TanStack Query are my go-to, let's look at a simple `useState` example. In applications using WebSockets or Server-Sent Events (SSE), we often need to update this state in real-time based on incoming events.

**Using an Array:**

```javascript
// react component
function ListComponent() {
  const [items, setItems] = useState([])

  useEffect(() => {
    // Initial fetch
    socket
      .service('items')
      .find()
      .then(({ data }) => setItems(data))
  }, [])

  useEffect(() => {
    // Listeners
    socket.service('items').on('created', (item) => {
      setItems([...items, item])
    })
    socket.service('items').on('patched', (item) => {
      setItems(items.map((t) => (t.id === item.id ? item : t)))
    })
    socket.service('items').on('removed', (item) => {
      setItems(items.filter((t) => t.id !== item.id))
    })
  })

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}
```

This works, but mapping over the entire array for every patch or filter for every removal feels inefficient and verbose.

## Using a Map Instead

ES6 introduced the `Map` object (see [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)), which holds key-value pairs. Using the `id` as the key simplifies updates significantly.

**Using a Map:**

```javascript
// react component
function ListComponent() {
  const [items, setItems] = useState(new Map())

  useEffect(() => {
    socket
      .service('items')
      .find()
      .then(({ data }) => {
        // Create Map from array: [id, item]
        setItems(new Map(data.map((d) => [d.id, d])))
      })
  }, [])

  useEffect(() => {
    ;['created', 'patched', 'removed'].forEach((event) => {
      socket.service('items').on(event, (item) => {
        if (event === 'removed') {
          items.delete(item.id)
        } else {
          items.set(item.id, item)
        }
        // Trigger re-render with new Map reference
        setItems(new Map(items))
      })
    })
  })

  return (
    <ul>
      {Array.from(items).map(([id, item]) => (
        <li key={id}>{item.name}</li>
      ))}
    </ul>
  )
}
```

The key is creating a new `Map` reference (`new Map(items)`) to trigger the state update. This approach simplifies the logic—no more mapping or filtering the entire collection just to update one item.

If you have feedback, reach out on [Twitter](https://twitter.com/_bskimball).

---

# Using Proxy ARP on pfSense to Bridge Remote Networks Across an IPSec Tunnel

> How to answer ARP requests for remote IPs using pfSense Virtual IPs and port forwarding. Useful for VPN tunnel access without renumbering subnets.

- Author: Brian Kimball
- Published: 2023-11-03
- Tags: pfsense, networking
- Canonical: https://brian-kimball.com/blog/pfsense-proxy-arp/
- Markdown: https://brian-kimball.com/blog/pfsense-proxy-arp/index.md

## What is Proxy ARP?

<figure>
  <blockquote>
    Proxy ARP is a technique by which a proxy server on a given network answers
    the Address Resolution Protocol (ARP) queries for an IP address that is not
    on that network.
  </blockquote>
  <figcaption>- Wikipedia</figcaption>
</figure>

Proxy ARP allows a firewall to answer requests for IP addresses that don't belong to it physically but are routed through it. I commonly use this to firewall off specific networks or allow access to resources across VPN tunnels—like the IPSec tunnel in this example.

## IPSec Tunnel

First, establish and verify the tunnel between the two networks. Ensuring identical configurations on both sides is key. For details, refer to the [pfSense IPSec documentation](https://docs.netgate.com/pfsense/en/latest/recipes/ipsec-s2s-psk.html).

![IPSEC tunnel](../../assets/proxy-arp_ipsec.png)

## Local Network

In this scenario, the LAN is `192.168.1.0/24`. We will add a new **Proxy ARP** Virtual IP on the pfSense.

![Virtual IP](../../assets/proxy-arp_virtual-ip.png)

Since we are defining a single IP, use a `/32` bitmask. This adds an entry to the firewall's ARP table. While the firewall won't reply to ICMP pings for this address, it will accept and route traffic destined for it.

## Forwarding to the Remote Network

The goal is to forward traffic from a LAN address (`192.168.1.30`) to a server on the remote network (`192.168.2.30`) across the tunnel.

We achieve this with a **Port Forwarding** rule.

![Port Forwarding](../../assets/proxy-arp_port-forward.png)

Configure the rule to forward traffic (TCP, UDP, ICMP, or specific ports) from the local Proxy ARP address to the remote server's IP. Once active, LAN users can access the remote server using the local address.

---

# Three Datacenters, One Career: From a Single Rack to Full Redundancy

> A 12-hour overnight migration, redundant Nexus switches, and LACP LAGG groups. How we built a datacenter that sustains sub-1ms latency and zero-downtime maintenance.

- Author: Brian Kimball
- Published: 2023-10-13
- Tags: datacenter, installation
- Canonical: https://brian-kimball.com/blog/datacenter-installations/
- Markdown: https://brian-kimball.com/blog/datacenter-installations/index.md

## Preface

Designing and implementing a datacenter remains one of the most challenging and fulfilling projects of my career. It has been three years since we installed the current iteration—our third location after moving from EvoSwitch in Manassas, VA, to AiNET in Beltsville, MD, and finally to CoreSite in Reston, VA.

## Initially in Manassas

Our first installation was a single rack in a co-location. We constantly watched the clock to avoid 495 rush hour traffic. The setup was simple:

- A single ASA firewall
- An IBM PureFlex system (managing Power and Intel nodes)
- A few IBM Power and Windows servers
- Cisco switches handling VLANs and networking

IBM eventually discontinued the PureFlex system, but at the time, it provided a "single pane of glass" for management.

## Next: AiNET

Growth and management issues in Manassas drove us to a location closer to our main office. Since the Flex system was discontinued, we moved individual servers and switched to redundant firewalls using pfSense.

We preferred pfSense over ASA for its flexibility, specifically the ability to run as a VM on VMware. We virtualized Intel workloads on VMware and Power workloads on IBM i. While we lost our single management pane (using vCenter for VMware and HMC for Power), we continued to expand.

My colleague Bill Harrison handled networking while I managed IBM Power configurations. We collaborated on VMware. We hosted mail servers, websites, and private servers, separated by VLANs and virtual firewalls. Although using DoD addresses caused some NAT-T issues, we learned a lot. When Bill moved on, I took over the infrastructure, having learned enough Cisco CLI to manage the growth.

## On to CoreSite

Before moving to CoreSite, we hired Tim, a Cisco Certified Instructor. Together, we planned a complete migration from AiNET. We spent months designing a redundant, high-speed infrastructure that eliminated DoD addresses and virtual firewalls. We opted for Netgate hardware for pfSense, ensuring every server had multiple connections to each switch.

### The Build

- **Firewalls:** High Availability (HA) pair of Netgate firewalls using pfSense.
- **Switches:** Pair of Cisco Nexus switches with LAGG groups to firewalls for redundancy.
- **VLANs:** Trunk mode ports to support virtualization.
- **VPC:** Configured on switches for consistency.

We tested the new setup with new servers, confirming throughput and power redundancy. Then, we scheduled a 12-hour overnight window for the move.

Tim, our coworker Jesse, and I drove to Beltsville, un-racked the equipment, and transported it to Reston. We immediately began racking.

- **Power Servers:** Connected via LACP LAGG groups to Nexus switches. (IBM documentation only confirmed this for Catalyst switches, but we made it work).
- **Intel Servers:** Network load balancing configured within VMware, bypassing the need for LAGG groups.

We separated the hypervisor and customer LPARs onto different VLANs. Once everything was tested and confirmed running, we met our deadline.

## Looking Back

I am very pleased with our final configuration. Maintenance is straightforward and easy to teach. Documentation allows us to add new networks and customers with minimal effort.

- **Performance:** IBM Power servers with 4-port LAGG show &lt;1ms latency to public services.
- **Storage:** VMware stack evolved into a vSAN solution with NVMe SSDs for speed and redundancy.
- **Uptime:** We can perform maintenance and apply firmware updates during the day without downtime.

This project stands as a highlight of my IT career.

---

# Advanced Job Scheduler

> The default IBM i job scheduler only runs daily, weekly, or monthly. Here is how the Advanced Job Scheduler finally unlocked hourly automation for my Node.js integrations.

- Author: Brian Kimball
- Published: 2023-10-12
- Tags: ibmi
- Canonical: https://brian-kimball.com/blog/advanced-job-scheduler/
- Markdown: https://brian-kimball.com/blog/advanced-job-scheduler/index.md

## Hourly jobs on IBM i?

Many of my recent backend projects involve pulling data from an API and inserting it into a database or emailing reports. These tasks, often related to EDI and e-commerce, typically run as Node.js applications executed by a CL program. To process orders efficiently, these programs need to run every hour—or even more frequently.

The default Job Scheduler on IBM i (accessed via `WRKJOBSCDE`) allows administrators to execute commands daily, weekly, or monthly.

![addjobscde screen](../../assets/addjobscde.png)
This standard scheduler offers limited options. In the past, I've seen
workarounds like creating multiple scheduled entries or writing complex CL
programs. With my Linux background, I prefer CRON jobs, which easily handle
specific intervals for recurring tasks.

### 5770-JS1: IBM's Advanced Job Scheduler

The Advanced Job Scheduler (licensed program 5770-JS1) has been available since IBM i v7r2. It requires a separate license and installation. You can access it via `WRKJOBJS` or add a job with `ADDJOBJS`.

![addjobjs screen](../../assets/addjobjs.png)
The `ADDJOBJS` screen shares many fields with the standard scheduler, but with a critical difference: it supports minute-based intervals. I can now schedule my Node.js applications to run hourly without any workarounds.

If you are running recurring integrations on IBM i and have not looked at the Advanced Job Scheduler, it is worth the license cost just to eliminate the scheduling hacks.

---

# A Modern Feathers.js Stack on IBM i: Real-Time APIs on DB2

> Real-time WebSockets, REST APIs, and a Vite React frontend running directly on IBM i using Feathers.js and a custom Knex adapter for DB2.

- Author: Brian Kimball
- Published: 2023-08-27
- Tags: development, reactjs, feathersjs, typescript, ibmi
- Canonical: https://brian-kimball.com/blog/feathers-ibm/
- Markdown: https://brian-kimball.com/blog/feathers-ibm/index.md

[Github Repo](https://github.com/bskimball/feathers-ibm)

## Preface

This project demonstrates a modern full-stack application on IBM i, featuring a Feathers.js backend and a Vite-powered SPA frontend. I also used it to test my custom Knex adapter for DB2 on i, [knex-ibmi](https://www.npmjs.com/package/@bdkinc/knex-ibmi). The frontend and backend communicate via WebSockets for real-time updates.

## Technical Decisions

**Backend**

The backend uses TypeScript and [Feathers.js](https://feathersjs.com/), a toolkit for creating applications with authentication, WebSockets (Socket.io), and REST APIs (Koa). It interacts with the DB2 database using [Knex](https://knexjs.org/), a powerful SQL query builder that works well with TypeScript.

To connect Knex with DB2 for i, I developed the [knex-ibmi](https://www.npmjs.com/package/@bdkinc/knex-ibmi) package. This configuration lets us scaffold services and hooks via the CLI, enabling modern real-time applications directly on IBM i.

**Frontend**

The frontend is a Single Page Application (SPA) built with TypeScript, Vite, and [React](https://react.dev/). Since Server-Side Rendering (SSR) wasn't required, I chose [Vite](https://vitejs.dev/) for bundling. The UI uses [shadcn/ui](https://ui.shadcn.com/), combining [Tailwind CSS](https://tailwindcss.com/) for styling and [Radix UI](https://www.radix-ui.com/primitives/docs/overview/introduction) for accessible components.

**Development**

The frontend and backend reside in separate directories for easier management. During development, I run their respective dev servers in separate terminal windows.

---

# Why Node.js Is My Default for Backend Automation

> Node.js dominates my automation stack because the ecosystem is massive and the training barrier is low. Here is how I structure scheduled backend jobs.

- Author: Brian Kimball
- Published: 2023-08-13
- Tags: development, node
- Canonical: https://brian-kimball.com/blog/node-automation/
- Markdown: https://brian-kimball.com/blog/node-automation/index.md

### Intro

Many business processes run on a schedule—MRP reports, EDI transactions, and database synchronizations. These tasks often run at night or at specific intervals to avoid disrupting operations. I frequently write programs to handle these automated interactions between databases and APIs.

### Why Node.js?

**Ecosystem & Packages**

The Node.js ecosystem is vast. Need to retrieve orders from Shopify or WooCommerce? There's likely a package for that. This availability significantly speeds up development compared to other languages.

**Training & Accessibility**

JavaScript/TypeScript and Node.js have immense training resources. It's easy for new developers to pick up, and installation is straightforward on most operating systems.

### Replacing Legacy Integrations

I often replace legacy applications written in Lansa. When APIs change or older apps fail, updating them in Lansa is often difficult due to a lack of modern libraries (like OAuth or standardized API clients). Rebuilding these integrations in Node.js is usually faster and more maintainable.

### Architecture

I typically write these as JavaScript applications using ESM imports.

- **No Compilation:** JSDoc provides sufficient typing without the need for a build step.
- **Structure:** Traditional MVC architecture, even without a view layer, keeps code organized.
- **Database:** I prefer [Knex](https://knexjs.org/) as a query builder, though I've also used [node-odbc](https://github.com/markdirish/node-odbc/) for raw SQL.

Since these applications handle sensitive data, they reside in private repositories. Once tested, a CL program triggers them via the operating system's scheduler.

---

# A Feathers.js Prototype for Customer Demographics and Automated Emails

> A proof-of-concept full-stack app using Feathers.js, React, Vite, and shadcn/ui to manage customer demographics and trigger anniversary and birthday emails.

- Author: Brian Kimball
- Published: 2023-07-16
- Tags: development, reactjs, feathersjs, typescript
- Canonical: https://brian-kimball.com/blog/reach-app/
- Markdown: https://brian-kimball.com/blog/reach-app/index.md

[Github Repo](https://github.com/bskimball/reach)

[App Demo](https://poc.lam.bdkcloud.com)

## Preface

I built this prototype as a proof of concept for a client who needed to manage customer demographic data. The goal was to attach detailed demographics to customer profiles and automate courtesy emails for anniversaries or birthdays. Existing services do this, but the client needed something that integrated with their custom software ecosystem.

## Technical Decisions

**Backend**

The backend uses **TypeScript** and **[Feathers.js](https://feathersjs.com/)**. Feathers provided a rapid way to spin up a Node.js server with REST APIs, WebSockets, and services/hooks for CRUD operations.

**Frontend**

The frontend is a **TypeScript** SPA built with **[Vite](https://vitejs.dev/)** and **[React](https://react.dev/)**. Since SSR wasn't required, I avoided the complexity of meta-frameworks.

- **UI:** **[shadcn/ui](https://ui.shadcn.com/)**, combining **[Tailwind CSS](https://tailwindcss.com/)** for styling and **[Radix UI](https://www.radix-ui.com/primitives/docs/overview/introduction)** for accessible components.

**Infrastructure**

The application runs on **Docker**.

- **Database:** MySQL.
- **Proxy:** **[Traefik](https://traefik.io/traefik/)**, chosen for its seamless Docker integration.

<div className="flex space-x-8">
  <div>![blog preview](../../assets/reach-app-form.png)</div>
  <div>![opportunity form](../../assets/reach-app-customer-view.png)</div>
</div>

---

# Link Aggregation on IBM i: LACP LAGG for Performance and Failover

> Step-by-step IBM i LAGG setup using *AGG resources and LACP policy, plus the gotcha we hit with Cisco Nexus VPCs.

- Author: Brian Kimball
- Published: 2023-02-26
- Tags: datacenter, ibmi
- Canonical: https://brian-kimball.com/blog/ibmi-lagg/
- Markdown: https://brian-kimball.com/blog/ibmi-lagg/index.md

## What and Why?

Link Aggregation (LAGG) combines multiple physical interfaces into a single logical interface. This configuration offers two key benefits:

1.  **Redundancy:** By connecting a single IBM server to multiple switches, you minimize downtime. If one switch fails, the other maintains the connection.
2.  **Performance:** When both switches are operational, traffic uses all available network cables, increasing throughput.

## How?

Configuration varies by switch manufacturer (e.g., Cisco Nexus uses Virtual Port Channels, or VPCs). On IBM i, follow these steps:

1.  **Identify Interfaces:** Determine which physical interfaces to aggregate. Typically, a 4-port network adapter uses labels like `CMN03`–`CMN06`. Verify this with the command `WRKHDWRSC *CMN`.

![ibmi-lagg-interfaces](../../assets/lagg-ports.png)

2.  **Create Line Descriptor:** Use the `WRKLIND` screen and press F6 (or use the `CRTLIND` command) to create a new line descriptor.

![ibmi-lagg-wrklind](../../assets/lagg-wrklind.png)

3.  **Configure Resource Name:** Instead of a specific port (e.g., `CMN04`), specify `*AGG` as the resource name.

4.  **Set Aggregate Policy:** Match this to your switch configuration.
    - For Cisco switches using LACP, select `*LNKAGG` on IBM i.
    - For standard EtherChannel, use `*ETHCHL`.

5.  **Set Policy Type:** `*SRCDESTP` is the standard and most common setting.

6.  **Define Aggregated Resources:** List the physical interfaces to be included in the LAGG group.

![ibmi-lagg-configure](../../assets/lagg-configure-line.png)

Once configured, the new line descriptor functions like any other. You can assign static IP addresses and routing as usual, but with the added benefits of performance and redundancy.

---

# Drawing Our Puppy as a Bluey Character

> A custom illustration of our puppy Cookie, rendered in the Bluey animation style to help the kids through a big family transition.

- Author: Brian Kimball
- Published: 2023-02-24
- Tags: illustration, hobby, design
- Canonical: https://brian-kimball.com/blog/cookie-bluey/
- Markdown: https://brian-kimball.com/blog/cookie-bluey/index.md

## Backstory

In early 2023, my girlfriend and I welcomed a new puppy, Cookie. Like most dogs, she is a very good girl. As we looked to buy a house and blend our families, I created this "Cookie as a Bluey character" poster to help the kids with the transition.

### Reception

Bluey is our daughter's favorite cartoon. As a parent, I appreciate its humor, short runtime, and valuable lessons. Our son loves the poster but has since requested a Fortnite version of Cookie. Our daughter has asked for a Barbie version. I might just create those too.

---

# Authenticating pfSense OpenVPN Against IBM i LDAP

> Use the IBM i Tivoli LDAP server as a single source of truth for VPN and service authentication. Here is the full setup, including a Node.js script to sync user profiles.

- Author: Brian Kimball
- Published: 2022-10-02
- Tags: ibmi, pfsense
- Canonical: https://brian-kimball.com/blog/ibmi-ldap/
- Markdown: https://brian-kimball.com/blog/ibmi-ldap/index.md

## Intro

When hosting services alongside IBM i, such as a VPN, unifying user authentication improves the end-user experience. By configuring the LDAP server on IBM i, users can log in to other services using their existing IBM i credentials.

## Process

First, add user profiles to the System Distribution Directory. LDAP, like SMTP, uses this directory for user information. You can access it via the `WRKDIRE` command.

**Note:** Usernames are limited to 8 characters due to legacy constraints.

Instead of manually keying each user, I wrote a script to transfer all IBM profiles to the System Distribution Directory automatically.

```javascript
import odbc from 'odbc'
import { Connection, CommandCall } from 'itoolkit'
import { parseString } from 'xml2js'

const config = {
  host: '<ip-address>',
  name: '<dsn-name>',
  username: '<user-name>',
  password: '<password>',
}

// set up ssh connection for running commands
const connection = new Connection({
  transport: 'ssh',
  transportOptions: {
    host: config.host,
    username: config.username,
    password: config.password,
  },
})

// connect via odbc using the DSN defined in odbc.ini
odbc.connect(`DSN=${config.name}`, (error, db) => {
  if (error) {
    throw error
  }

  // query the QSYS2 user info file
  db.query('SELECT USER_NAME,TEXT FROM QSYS2.USER_INFOB', (error, result) => {
    if (error) {
      throw error
    }

    // comb through the results
    result.forEach(({ USER_NAME, TEXT }) => {
      // disregard the IBM defined profiles
      if (USER_NAME.startsWith('Q')) {
        return false
      }

      // USER ID has an 8 character limit
      const USER_ID = USER_NAME.substring(0, 8)
      console.log({ USER_ID })
      // set up the command to run
      const command = new CommandCall({
        type: 'cl',
        command: `ADDDIRE USRID(${USER_ID} ${config.name}) USRD('${TEXT}') USER(${USER_NAME})`,
      })

      // add the command to the connection
      connection.add(command)
    })

    // after we added the commands let's run them
    connection.run((error, xmlOutput) => {
      if (error) {
        console.log({ error })
      } else {
        // parse the results
        parseString(xmlOutput, (parseError, result) => {
          if (parseError) {
            console.log({ parseError })
          }
          console.log({ result })
        })
      }
    })
  })
})
```

## Managing LDAP

IBM integrates the Tivoli LDAP server, which is configurable via the Web Navigator. For detailed instructions, refer to the [IBM i LDAP documentation](https://www.ibm.com/docs/en/i/7.5?topic=server-configuring-directory). Like standard LDAP servers, it uses Common Names (CN) and Organization Names (ON) for authentication integration.

![IBMi LDAP Properties](../../assets/ibmi-ldap-properties.png)

## Implementation

I most commonly use this to authenticate pfSense OpenVPN users. Configuring pfSense to authenticate against the IBM i LDAP server is straightforward. Once set up, you can assign this authentication method to your OpenVPN configuration. This approach works for many other services that support LDAP.

![PFSense LDAP](../../assets/pfsense-ldap.png)

The full Node.js script for syncing profiles is available in the snippets above. If you are consolidating auth across IBM i and external services, LDAP is often the simplest bridge without adding a separate identity provider.

---

# A Network Discovery App for MSP Client Onboarding

> A Node.js tool to automate nmap scans, whois lookups, and DNS queries during MSP onboarding. Built for the Raspberry Pi with Fastify and Solid.js.

- Author: Brian Kimball
- Published: 2022-08-12
- Tags: development, solidjs, fastify
- Canonical: https://brian-kimball.com/blog/network-mapper/
- Markdown: https://brian-kimball.com/blog/network-mapper/index.md

[Github Repo](https://github.com/bskimball/network-mapper)

## Preface

For Managed Service Providers (MSPs), gathering detailed network information during client onboarding is critical. This process is usually time-consuming and non-billable. To solve this, I built a Node.js application to automate routine discovery tasks.

The concept is simple: install the app on a Raspberry Pi, connect it to the prospective client's network, and initiate a scan. The app runs `nmap` to discover local devices and performs `whois` lookups on configured domains. It also queries DNS records (like MX records) to identify email providers. Technicians can then submit the gathered data to a central repository via a form.

## Technical Decisions

**Architecture**

I built this as a monolithic JavaScript application using ESM imports, with no server-side compilation step. It connects to a PostgreSQL database running in Docker.

**Stack**

I prioritized performance and speed.

- **Server:** [Fastify](https://fastify.io/), for its low overhead.
- **UI:** [Solid.js](https://www.solidjs.com/), for its fine-grained reactivity and small bundle size.

This minimal full-stack JavaScript setup is highly efficient. The frontend consumes a REST API from Fastify, while WebSockets push live scan updates to the client.

---

# Building a Hosting Brand Site with Next.js and Real DC Photography

> We needed a dedicated, marketable site for our hosting services. I used actual data center photography and Next.js to build it.

- Author: Brian Kimball
- Published: 2022-01-14
- Tags: design, nextjs
- Canonical: https://brian-kimball.com/blog/bdkcloud-website/
- Markdown: https://brian-kimball.com/blog/bdkcloud-website/index.md

[Website](https://bdkcloud.com/)

[Github Repo](https://github.com/bdkinc/bdkcloud)

## Backstory

We needed to sharpen our focus on hosting operations. The goal was a dedicated, marketable website for our hosting services. I designed the landing page using actual images from the data center that houses our equipment.

## Technical Decisions

I previously used Nuxt.js for websites. While I love Vue's performance, I prefer the flexibility of returning JSX from functions over templates. Switching to Tailwind CSS also removed the need for Vue's style sections.

Since React is more prevalent in the industry, and I lead training resources, switching to React minimized our training footprint.

For a website, SEO and performance are mandatory. I chose Next.js for its SSR capabilities and React UI. The site runs in a Docker container behind a Traefik proxy.

---

# Tracking Warehouse Inventory with Laravel, Vue.js, and Barcode Scanning

> A vinyl fencing company needed a warehouse inventory system. I built a Vue.js SPA with barcode scanning and shareable filters, backed by a Laravel API.

- Author: Brian Kimball
- Published: 2022-01-08
- Tags: development, vuejs, laravel
- Canonical: https://brian-kimball.com/blog/inventory-tracker/
- Markdown: https://brian-kimball.com/blog/inventory-tracker/index.md

[Github Repo](https://github.com/bskimball/inventory-tracker)

## Preface

A vinyl fencing company needed a simple system to track warehouse inventory. Requirements included scanning vendor barcodes to receive items, looking up inventory, filtering with shareable URL-synced links, and a pre-programmed interface for different item types.

## Technical Decisions

**Architecture**

I chose a decoupled architecture: a Vue.js SPA frontend communicating with a Laravel API backend.

**Backend**

[Laravel](https://laravel.com/) provides a complete, robust framework. Inspired by Rails but built on PHP, it enables a single developer to build full-stack applications efficiently.

**Frontend**

The frontend uses Vue.js and Vite.

- **Vite:** Faster and easier to configure than Laravel Mix or Webpack.
- **Vue.js:** Excellent performance and reactive state management.
- **UI:** Bootstrap 5, which integrates well with Vite + Vue and offers utility and component classes.

**Deployment**

I use Docker to deploy both frontend and backend containers to an Ubuntu server. A Traefik proxy manages access, with NGINX behind it to serve both applications from the same domain. While Traefik can handle this, the NGINX container improves portability.

---

# NFT PFP Idea

> Experimenting with profile-picture art styles led to this character concept—part illustration exercise, part curiosity about digital ownership.

- Author: Brian Kimball
- Published: 2021-11-21
- Tags: illustration, hobby, design
- Canonical: https://brian-kimball.com/blog/nft-idea/
- Markdown: https://brian-kimball.com/blog/nft-idea/index.md

NFTs and crypto have surged in popularity. While I remain skeptical about the long-term viability of some crypto assets, I appreciate the underlying technology. NFTs, in particular, have provided artists with a unique digital outlet. Inspired by the creativity of NFT profile pictures on Twitter, I decided to design one myself.

---

# A Badge-Scanning Time Tracker for Warehouse Floors

> Warehouse employees scan badges or barcodes to log tasks. I built a Vue.js + Laravel app with URL-synced filters for easy reporting.

- Author: Brian Kimball
- Published: 2021-10-04
- Tags: development, vuejs, laravel
- Canonical: https://brian-kimball.com/blog/time-tracker/
- Markdown: https://brian-kimball.com/blog/time-tracker/index.md

[GitHub Repo](https://github.com/bskimball/time-tracker)

## Preface

A warehouse needed a streamlined way to track employee hours and tasks. They wanted employees to scan badges or barcodes to log start and end times for specific tasks. I created an interface tailored to their workflow, including a filtering system that syncs with the URL for easy reporting and sharing.

## Technical Decisions

**Architecture**

I chose a decoupled architecture: a Vue.js SPA frontend communicating with a Laravel API backend.

**Backend**

[Laravel](https://laravel.com/) provides a complete, robust framework. Inspired by Rails but built on PHP, it enables a single developer to build full-stack applications efficiently.

**Frontend**

The frontend uses Vue.js and Vite.

- **Vite:** Faster and easier to configure than Laravel Mix or Webpack.
- **Vue.js:** Excellent performance and reactive state management.
- **UI:** Bootstrap 5, which integrates well with Vite + Vue and offers utility and component classes.

**Deployment**

I use Docker to deploy both frontend and backend containers to an Ubuntu server. A Traefik proxy manages access, with NGINX behind it to serve both applications from the same domain. While Traefik can handle this, the NGINX container improves portability.

If you are building similar warehouse tools, Laravel's queue system pairs well with barcode scanners for real-time job tracking. The repo is available if you want to see the full Docker setup.
