Bruno GalvánLima, Peru
Section 1 of 7: What I do
SaaS platforms
Products with organizations, scheduling and billing, from the schema to the screen.
Multi-tenant
Typed APIs
Contract-first REST in hexagonal layers, with domain events through an outbox.
Fastify · OpenAPI
Product interfaces
Calendars, billing screens and editors with optimistic updates.
React 19 · TanStack Query
AI in the product
LLM features where the work happens: bilingual documents and a booking assistant.
OpenAI · WhatsApp
Section 3 of 7: Stack
The toolchain
Every tool here appears in my own commits from the last two years.
Typed end to end
Strict TypeScript from the schema to the screen.
Tested as shipped
Vitest and Playwright against the production build.
Section 4 of 7: Method
Dependency runs one way; the domain knows nothing of the framework.
Explicit boundaries
No abstraction for a future that has not arrived.
The smallest change
Done means the gates are green and it was seen in production.
Verify before claiming
What the code cannot say goes into a dated record.
Decisions in writing
Section 5 of 7: Code
import { roles, type Month } from './experience';
import { projects } from './projects';
import { stack } from './stack';
// Every figure the site states is computed here from the record, at build
// time, so a number can never contradict the roles and projects under it.
const toMonths = (month: Month) => {
const [year, index] = month.split('-').map(Number);
return (year ?? 0) * 12 + (index ?? 1) - 1;
};
const monthOf = (date: Date): Month =>
`${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}` as Month;
// How long a role lasted, in whole months, counting its first and last month;
// an open role runs to the current month.
export function roleMonths(
role: { start: Month; end: Month | null },
today: Date,
) {
const end = role.end ? toMonths(role.end) : toMonths(monthOf(today));
return end - toMonths(role.start) + 1;
}
export function figures(today: Date) {
const now = toMonths(monthOf(today));
const first = Math.min(...roles.map((role) => toMonths(role.start)));
return {
yearsSinceFirstRole: Math.floor((now - first) / 12),
firstYear: Math.floor(first / 12),
companies: new Set(roles.map((role) => role.company)).size,
projects: projects.length,
publicProjects: projects.filter((project) => project.repository).length,
technologies: Object.values(stack).flat().length,
};
}
// One entry per month from the first role to today: the role held that month
// and how many months into it, or null for a month between roles.
export function career(today: Date) {
const now = toMonths(monthOf(today));
const first = Math.min(...roles.map((role) => toMonths(role.start)));
return Array.from({ length: now - first + 1 }, (_, offset) => {
const month = first + offset;
const role = roles.find(
(candidate) =>
toMonths(candidate.start) <= month &&
month <= (candidate.end ? toMonths(candidate.end) : now),
);
return {
year: Math.floor(month / 12),
month: month % 12,
role: role?.id ?? null,
tenure: role ? month - toMonths(role.start) + 1 : 0,
};
});
}---
// The section statement: two short sentences between decorative slashes. The
// slashes are generated content with an empty text alternative, so they are
// neither read aloud nor measured as text. The name lives on the heading and
// the visual copy is hidden, so the per-character reveal never reaches
// assistive technology and the phrase reads whole.
interface Props {
lines: readonly string[];
id?: string;
level?: 'h1' | 'h2' | 'h3';
size?: 'headline' | 'display';
}
const { lines, id, level: Tag = 'h2', size = 'headline' } = Astro.props;
---
<Tag
class:list={['headline', `text-${size}`]}
id={id}
aria-label={lines.join(' ')}
>
<span aria-hidden="true" data-reveal>
{lines.map((line, index) => (
<>
{index > 0 && <br />}
{index < lines.length - 1 ? `${line} ` : line}
</>
))}
</span>
</Tag>
<style>
.headline {
margin: 0;
text-wrap: balance;
}
.headline::before,
.headline::after {
color: var(--decor);
font-family: var(--font-pixel);
}
.headline::before {
content: '/\00a0' / '';
}
.headline::after {
content: '\00a0/' / '';
}
</style> for (const colorScheme of ['light', 'dark'] as const) {
test(`${locale}: accessible ${colorScheme} page without overflow`, async ({
page,
}) => {
await page.emulateMedia({ colorScheme, reducedMotion: 'reduce' });
await page.goto(`/${locale}/`);
await expect(
page.getByRole('button', { name: messages(locale).theme.darkMode }),
).toHaveAttribute('aria-pressed', String(colorScheme === 'dark'));
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(results.violations).toEqual([]);
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth,
),
).toBe(true);
});
}Section 6 of 7: Projects
ClinicSay
Healthcare SaaS. I build billing, scheduling and documents across the React app and the Fastify API.
Private codeVitalPro
My own multi-tenant SaaS for service businesses: an Nx monorepo with Next.js and Fastify.
Private codebrunogalvan.dev
This site: static Astro in two languages, a CSP built from hashes, and axe in both themes.
Placer Sano
A restaurant site in Astro: menu, reservations and team.
Section 7 of 7: Experience
— Present
ClinicSay
Senior Full Stack Developer
Healthcare SaaS: billing, scheduling and documents across React 19 and a hexagonal Fastify API.
—
Métrica Andina
Senior Analyst Programmer
Microservices, a Salesforce REST integration and WhatsApp automation for education clients.
—
Surtidores S.A.C.
PHP Programmer
Led the new web ERP in Laravel; kept the previous one, on PHP 5, running.
—
Platanitos
Web Programmer · SAP Analyst
Web applications for retail, electronic invoicing, and reports joining SAP with SQL.