Hello, I am Reo Hakuta (GitHub @ReoHakase), a third-year Information Science student at Kyoto Institute of Technology, graduating in 2027.
From August 16 to September 15, 2025, I joined PLAID's Developer Experience & Performance team for a one-month summer internship. This article covers what I delivered, how I worked, and what I learned.

👤 About me
I usually build full-stack TypeScript applications and often work on frontend and backend systems in Turborepo monorepos. I particularly enjoy frontend work, from creating designs in Figma to optimizing applications with React Server Components and TanStack Query.
One example is reaching the DCON2025 final organized by JDLA and NHK. Our team placed fifth out of 95 teams and received the Topy Industries Award, Hitachi Industry & Control Solutions Award, and Quick Award.
- Locker.ai: an automated lost-property service combining an LLM with smart lockers
- Full-stack TypeScript development with Next.js, NestJS, and GraphQL
I graduated from the National Institute of Technology, Ibaraki College and transferred into the third year at Kyoto Institute of Technology. PLAID was the second company where I had taken part in a paid internship.
📜 Why I joined
I was already interested in Developer Experience. In my own projects, I had used GitHub Actions, Vitest, Storybook with an accessibility plugin, and Playwright for CI, and had used Turborepo caching to make CI more efficient. At the same time, I felt that a personal project could not provide meaningful experience with a genuinely large codebase. I discussed this with PLAID representatives at a Supporterz one-on-one event at the end of March, which led to the summer internship.
PLAID's KARTE codebase, in the karte-io-systems repository, was also a strong match for my interests: it was a very large monorepo implemented mainly in TypeScript.
The repository contained multiple pnpm workspaces under systems/*, each holding several packages. It had more than 296,000 commits, and Issue and pull request numbers had reached approximately 133,000.
. (karte-io-systems)
├── .git
├── package-lock.json
├── package.json
├── README.md
└── systems
├── academy2
├── action-editor
├── action-table
├── admin
├── apiv2
└── ... (36 systems in total)✨ What I delivered
The Developer Experience & Performance team improves the codebase across product boundaries. Consequently, the scope and size of my tasks varied considerably.
karte-io-systems- Added
@plaidev/eslint-plugin-express(#133382) - Added Namespace profile names to the actionlint configuration (#132929)
- Moved the
karte-ai-testexecution environment from GitHub Actions to Namespace to improve cache efficiency (#132849) - Removed aliases from the KARTE AI Vitest configuration (#132603)
- Consolidated dependencies into pnpm catalogs with
pncat(#132587) - Extracted path conversion in
karte-ai-testCI into a separate step (#132545) - Enabled the recommended and stylistic typescript-eslint rules in
old-karte/front-react(#132117) - Configured Vitest Test Projects and changed-file testing for KARTE AI (#132071)
- Migrated
old-karte/front-reactto ESLint v9 (#131871) - Added appropriate ARIA roles and keyboard interaction to
EventItem(#131736) - Replaced a
divandlabelwithfieldsetandlegendinSegmentConditionBuilder(#131732) - Renamed the
MessageBubbleproproletotypeto avoid conflicts with accessibility rules (#131728) - Prevented ESLint and type-check processes from terminating abnormally and enabled both checks in CI for
old-karte/front-react(#131656) - Fixed CI so package-level
lintandlint:i18ntasks ran inold-karte(#13145)
- Added
eslint-plugin- Created
@plaidev/eslint-plugin-express,@plaidev/eslint-plugin-hono, and@plaidev/eslint-plugin-trpc(#4) - Created a Turborepo monorepo environment (#2)
- Created
sour-mcp-server- Added support for the latest Tailwind CSS v4-compatible
sour-tailwind(#6)
- Added support for the latest Tailwind CSS v4-compatible
karte-io-terraform- Configured Trivy locally and in GitHub Actions for
terraform/aws/evaluationandgcp/evaluation-karte-io-systems(#1448)
- Configured Trivy locally and in GitHub Actions for
The following five projects were the most instructive.
1. Running only affected tests with Vitest Test Projects and vitest related
Background
The KARTE AI pnpm workspace is used to validate and develop AI features in the KARTE product family. Its Mastra server tests include LLM behavior checks, so every CI run could take a long time. I set out to run only the tests related to changed files.
karte-io-systems/systems/karte-ai/
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
├── apps/
│ ├── backend/ # Mastra server
│ ├── front/ # React administration UI
│ ├── web/ # Backend API server
│ └── *-mcp/ # MCP servers
└── packages/
├── shared/ # Shared code
├── shared-server/ # Shared server code
└── trpc/ # API schemaPreviously, Turborepo invoked Vitest independently in each package. There were twelve separate vitest.config.ts files.
Two Vitest features were relevant:
vitest related <file>runs tests for a specified file and the files that depend on it.vitest run --changed=<ref>finds every file changed from a branch or commit and applies the same related-test logic.
In a monorepo, Vitest also has to follow dependencies across internal packages declared with workspace:. I first configured Vitest Test Projects, consolidating the package configurations at the workspace root and inlining internal @karte-ai/* packages.
const alias = {
'@karte-ai/shared': path.resolve(__dirname, './packages/shared/src/index.ts'),
'@karte-ai/shared-server': path.resolve(__dirname, './packages/shared-server/src/index.ts'),
'@karte-ai/trpc': path.resolve(__dirname, './packages/trpc/src/index.ts'),
};
export default defineConfig({
test: {
projects: [
{
extends: true,
test: {
name: 'backend',
include: ['apps/backend/**/*.test.{ts,js}'],
includeSource: ['apps/backend/src/**/*.{js,ts,jsx,tsx}'],
globals: true,
environment: 'node',
},
},
// The other eleven projects follow the same structure.
],
alias,
server: { deps: { inline: [/@karte-ai\\//] } },
},
});This allowed Vitest to track file-level dependencies across package boundaries. vitest run --changed=develop could then identify the tests affected by a pull request. The final implementation used Node.js Type Stripping and pointed exports.default to the pre-build .ts entry points, avoiding the aliases shown above.
There was another constraint: vitest run --changed=<ref> relies on git diff --name-only. CI therefore needs enough history to find the merge base between develop and the pull request branch; fetch-depth: 1 is insufficient.
I experimented with sparse checkout and --shallow-exclude, but checkout still took more than 50 seconds in a repository with over 296,000 commits. Instead of fetching additional Git history, I used the GitHub REST API through tj-actions/changed-files@v46 with use_rest_api: true. CI converted the returned repository paths to paths relative to systems/karte-ai and passed them to vitest related.
- name: Get changed files
if: ${{ github.event_name == 'pull_request' }}
id: calc_changed
uses: tj-actions/changed-files@v46
with:
token: ${{ secrets.GITHUB_TOKEN }}
use_rest_api: true
files: |
systems/karte-ai/**
- name: Test related files
if: ${{ github.event_name == 'pull_request' }}
run: pnpm exec vitest related ${{ steps.convert_paths.outputs.files }} --run
- name: Test all files
if: ${{ github.event_name != 'pull_request' }}
run: pnpm test:allRetrieving the changed-file list took about two seconds. Pull request events ran only related tests, while pushes to develop continued to run the full suite as a safety measure.
Test Files 3 passed | 1 skipped (4)
Tests 7 passed | 3 skipped (10)
Duration 37.65s2. Developing internal ESLint v9 packages
I implemented internal ESLint plugins for Express, Hono, and tRPC and published them as internal packages. To make future plugins easier to maintain, the project used:
- A Turborepo monorepo that can accommodate multiple internal plugins
- GitHub Actions, Changesets, and pnpm for releases and publishing
- Vitest and
eslint-vitest-rule-testerfor valid and invalid test cases - Biome, Lefthook, and tsdown
One rule, eslint-plugin-hono/no-project-id-from-context, forbids using a project ID read from Hono's c.req.json(). It instead directs developers to safer sources:
c.get('currentProject')._id, derived from the authenticated user's current project- A server-managed environment variable
- A database lookup based on authenticated information
I implemented equivalent user_id and api_key rules, corresponding Express and tRPC plugins, and their tests. The eslint-vitest-rule-tester helpers let the test cases closely mirror the requirements.
describe("no-project-id-from-context", () => {
const { valid, invalid } = createRuleTester({
name: "no-project-id-from-context" satisfies RuleName,
rule: noProjectIdFromContext,
languageOptions,
})
it("allows the authenticated current project ID", async () => {
const { result } = await valid({
code: `const currentProject = c.get('currentProject');
await handler({ projectId: currentProject._id });`,
})
expect(result).toMatchSnapshot()
})
it("rejects a project ID from the request body", async () => {
const { result } = await invalid({
code: `const body = await c.req.json();
await handler({ projectId: body.project_id });`,
errors: [{ messageId: "fromContext" }],
})
expect(result).toMatchSnapshot()
})
})Snapshots also covered the reported location and error message. After publishing, the plugins were introduced into several workspaces.

3. Automating a pnpm catalog migration with pncat and contributing upstream
I used the pncat CLI to automate a migration to pnpm catalog, centralizing dependency versions in a monorepo. The migration covered two pnpm workspaces and changed 47 files in total.
Background
Previously, each package.json stored its own versions. Different versions of the same package could coexist and install duplicate dependencies, and upgrades required edits in several places. A pnpm catalog offered a central source of truth for shared versions.
Approach
I installed pncat and configured all packages to use the unnamed default catalog.
pnpm i -Dw pncatpnpm supports an unnamed catalog (catalog: or catalog:default) and named catalogs (catalog:<name>). Named catalogs are useful when a monorepo must retain multiple major versions. Categorizing packages by labels such as test, form, or validator, however, would allow the same dependency to be specified more than once without providing a clear benefit. I therefore overrode pncat's category-oriented default rules.
import { defineConfig } from "pncat"
export default defineConfig({
catalogRules: [{ name: "default", match: [/.+/] }],
})The migration commands were:
# Expand catalog references back into explicit versions when necessary.
pnpm pncat revert
# Migrate explicit versions into the configured catalog.
pnpm pncat migrateWhen pncat detected several versions, it prompted for the version to retain. After migration, pnpm-workspace.yaml held the central catalog. I enabled catalogMode: strict to require catalog use and cleanupUnusedCatalogs: true to remove unused entries automatically.
catalog:
"@ai-sdk/react": 1.2.12
"@ark-ui/react": 5.15.1
"@fortawesome/react-fontawesome": 0.2.2
catalogMode: strict
cleanupUnusedCatalogs: trueEach package.json then referred to the catalog instead of repeating a version:
{
"dependencies": {
"@ai-sdk/react": "catalog:",
"@ark-ui/react": "catalog:",
"@fortawesome/react-fontawesome": "catalog:"
}
}The two systems moved 127 and 90 packages respectively under catalog management. Centralizing versions and removing duplicates reduced the work required for dependency updates.
In pnpm v10.15.1, pnpm remove did not remove an entry from the catalog until the next pnpm install; a fix was planned for v10.15.2 in pnpm/pnpm#9930.
I also contributed two fixes to pncat after noticing that it emitted the redundant catalog:default instead of catalog::
- Use
catalog:instead ofcatalog:defaultfor the default catalog - Use
catalog:in detection and migration as well
4. Evaluating a move from GitHub Actions to Namespace for better caching
I moved the karte-ai-test execution environment from GitHub Actions to Namespace and evaluated its caching. Test execution fell from 1 minute 14 seconds to 32 seconds, a 2.3-times speedup.
Container initialization and pnpm install had dominated the original job:
- With a cache hit,
pnpm installtook about 17 seconds. - With a cache miss,
pnpm installtook about 52 seconds, followed by about 45 seconds to upload the cache withactions/cache.
When dependencies changed frequently, a cache this slow offered little benefit. Repository-level capacity limits also prevented the GitHub Actions cache from being used effectively.
Namespace advertises runners with built-in Docker image caching and high-throughput local NVMe caching, avoiding cache upload and download bandwidth. Its Cache Volumes are designed to persist data between GitHub Actions runs with cache locality. I changed the runner with one line and added Namespace's persistent cache actions for pnpm and Turborepo.
jobs:
karte-ai-test:
runs-on: namespace-profile-ubuntu2404-amd64-16core
steps:
- name: Set up pnpm cache
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: pnpm
- name: Set up Namespace Turborepo cache
uses: namespace-actions/setup-turbocache@v0This reduced the job to 32 seconds.

Namespace also provides namespacelabs/nscloud-checkout-action, but it did not support sparse checkout. Even with fetch-depth: 1 and caching, checkout took about 15 seconds in karte-io-systems, compared with about two seconds for actions/checkout with sparse checkout. I therefore kept the latter.
5. Trialing Terraform static analysis with Trivy
For Terraform in the evaluation environments aws/evaluation and gcp/evaluation-karte-io-systems, I compared Trivy and Checkov as candidates for trial static analysis.
A usable VS Code extension was important to the Developer Experience. The Prisma Cloud extension for Checkov required a Prisma Cloud API key and, according to the source I found, access was paid. A community-maintained open-source Checkov extension also existed, but I could not get it to work, so I selected Trivy.

- Official Trivy VS Code extension (Apache-2.0)
- Official Checkov / Prisma Cloud VS Code extension (closed source)
- Community Checkov VS Code extension (Apache-2.0)
I then configured trivy.yaml, the VS Code extension, and GitHub Actions.


💼 Working environment
PLAID combines office and remote work with flexible hours. I worked at the GINZA SIX office during the first week, then mostly worked remotely from my home in Kyoto for an average of about 40 hours per week. The flexible schedule made it easier to accommodate university commitments during the day.
🚅 Travel and accommodation
For that year's summer internship, PLAID reimbursed up to ¥25,000 per month for travel and up to ¥50,000 for accommodation when working from the office. That was sufficient for my first week in Tokyo. These terms may change for future internships.
💻 Development environment
PLAID provided:
- A 16-inch M2 Max MacBook Pro with 96 GB of memory
- A Cursor Team Plan account
- A Claude Code account through Google Vertex AI
- An OrbStack Pro account
This let me work with an environment close to my usual setup.

🏢 The office
PLAID's office occupies the tenth floor of GINZA SIX, directly connected to Ginza Station. It included a large coffee machine, water dispensers, meeting booths, and an event area with artificial grass. Desks were unassigned, so I often sat beside my on-the-job trainer and worked while asking questions and chatting. Most monitors were 4K or 5K. Ice, dairy milk, soy milk, and almond milk were available, and I often made a café latte while working.


🍽️ Welcome Lunch program
When a meal included a new employee or intern, the Welcome Lunch program provided up to ¥2,000 per person. It gave me opportunities to meet employees and other interns at restaurants around Ginza that I would not normally visit as a student.



💭 Reflections
My previous internship at another company had been fully remote, so this was my first internship that included time in an office. Working in person gave me more opportunities to interact with team members, eat together, and join the company-wide monthly “shimemeshi” gathering. It made the month particularly meaningful and enjoyable. Thank you to everyone at PLAID for the experience.
