perf(credshare): list a team's vaults in one query instead of two per vault (#128)
Some checks are pending
CI / build (push) Waiting to run
test / test (push) Waiting to run

GET /api/credshare/teams/:slug/vaults built its response in a loop, asking
the database for a grant row and a secret count once per vault. libSQL is
remote, so each of those is a network round trip, and the endpoint cost
2N+1 of them.

On a team with 176 vaults that is 353 round trips and ~10.6s of server
time. `logicsrc teams pull` resolves the vault id twice -- once planning
the sync, once reading values -- so a pull of a single ten-key vault took
~24s, nearly all of it spent listing vaults the command does not want.

Replaced with one SELECT carrying two correlated subqueries. Both are
covered by existing primary keys (credshare_secrets is keyed
(vault_id, name), credshare_vault_grants (vault_id, user_id)), so the
per-vault work becomes an index probe inside the database instead of a
round trip across the network. No schema or index change.

Measured on a local libSQL seeded to match that team -- 176 vaults, 17
secrets each -- the endpoint goes from 355 round trips to 3, and returns
identical rows.

The response shape is unchanged: hasAccess is still a real boolean rather
than the 0/1 SQLite hands back, and secretCount is still a number.

Tests pin behaviour and cost separately. The behavioural cases pass
against both the old loop and the new query, which is the point -- only
the round-trip count changed. The regression guard asserts the query
count for 3 vaults EQUALS the count for 30 rather than matching a magic
number, so any future rewrite that reintroduces per-vault I/O fails no
matter what the constant part costs. Against the old loop it reports
9 vs 63.

Two sibling endpoints have the same shape -- /teams/:slug/members and
/vaults/:id/grants both call publicKeyFor() per member. Neither is on the
pull path and both scale with member count rather than vault count, so
they are left alone here.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-08-03 22:20:31 -07:00 committed by GitHub
parent 4c88155f08
commit fd253b0485
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 196 additions and 8 deletions

View file

@ -157,14 +157,33 @@ credshareRouter.post("/api/credshare/invites/accept", api(async (req, res, user)
// ---- vaults ----
credshareRouter.get("/api/credshare/teams/:slug/vaults", api(async (req, res, user) => {
const ctx = await requireMember(res, req.params.slug, user.id); if (!ctx) return;
const vaults = await all(`SELECT * FROM credshare_vaults WHERE team_id = ? ORDER BY name`, [ctx.team.id]);
const out = [];
for (const v of vaults) {
const grant = await get(`SELECT 1 FROM credshare_vault_grants WHERE vault_id = ? AND user_id = ?`, [v.id, user.id]);
const count = await get(`SELECT COUNT(*) AS n FROM credshare_secrets WHERE vault_id = ?`, [v.id]);
out.push({ id: v.id, name: v.name, hasAccess: Boolean(grant), secretCount: Number(count?.n || 0) });
}
res.json({ vaults: out });
// One statement, not one per vault. libSQL is remote, so every execute() is a
// network round trip: the previous loop cost 2N+1 of them, and a team with 176
// vaults spent ~10s here -- doubled by `teams pull`, which resolves the vault
// id twice. Both correlated subqueries are covered by existing primary keys
// (credshare_secrets is keyed (vault_id, name), grants (vault_id, user_id)),
// so this is an index scan per vault inside the database rather than a
// round trip per vault across the network.
const vaults = await all(
`SELECT v.id,
v.name,
(SELECT COUNT(*) FROM credshare_secrets s
WHERE s.vault_id = v.id) AS secret_count,
EXISTS(SELECT 1 FROM credshare_vault_grants g
WHERE g.vault_id = v.id AND g.user_id = ?) AS has_access
FROM credshare_vaults v
WHERE v.team_id = ?
ORDER BY v.name`,
[user.id, ctx.team.id]
);
res.json({
vaults: vaults.map((v) => ({
id: v.id,
name: v.name,
hasAccess: Boolean(v.has_access),
secretCount: Number(v.secret_count || 0)
}))
});
}));
credshareRouter.post("/api/credshare/teams/:slug/vaults", api(async (req, res, user) => {