agentsclimarketplace

Data access layer

Skill param087/saas-starter-skills/skills/data-access-layer

Production-grade full-stack SaaS skills for AI coding agents — Next.js, Postgres/Drizzle, Auth, Stripe & Vercel patterns for Codex, Claude Code, Cursor & OpenCode.

Install
npx -y skills add param087/saas-starter-skills --skill data-access-layer

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when querying or mutating the database from the app — wrap Drizzle in server-only, tenant-scoped functions instead of calling the ORM from components, so every read/write is typed, testable, and isolation-safe.

SKILL.md

2.8 KB, as published. Nobody here has run it

Data Access Layer

Overview

Don't scatter ORM calls across components and route handlers. Put every query behind a server-only data-access layer (DAL): small typed functions that take an explicit orgId and never return another tenant's rows. The rest of the app calls getProjects(orgId), not db.select(...). This centralizes tenant scoping, caching, and the place you write tests.

When to use

  • Any read or write to the database.
  • You catch yourself importing db into a React component.
  • You want one place to enforce "every query is scoped to the current org".

The pattern

// src/server/services/projects.ts
import "server-only";
import { and, eq, desc } from "drizzle-orm";
import { db } from "@/server/db/client";
import { projects } from "@/server/db/schema";

export async function listProjects(orgId: string) {
  return db.select().from(projects)
    .where(eq(projects.orgId, orgId))
    .orderBy(desc(projects.createdAt));
}

export async function getProject(orgId: string, id: string) {
  const [row] = await db.select().from(projects)
    .where(and(eq(projects.id, id), eq(projects.orgId, orgId)))  // scope by BOTH
    .limit(1);
  return row ?? null;
}

export async function createProject(orgId: string, input: { name: string }) {
  const [row] = await db.insert(projects).values({ orgId, ...input }).returning();
  return row;
}

Callers (server components, actions, route handlers) pass the orgId resolved from the session — they never invent it.

Why a DAL

  • Isolation by construction. Lookups filter on id AND org_id, so a guessed UUID from another tenant returns null, not data.
  • Typed end-to-end. returning() gives you the row type for free.
  • Testable. Mock or seed the DB and test functions directly, no HTTP needed.
  • Cacheable. Wrap reads in cache() / unstable_cache in one spot.

Pitfalls

  • Calling db from components — couples UI to the schema and risks shipping queries to the client. Always go through the DAL.
  • Lookups by id onlywhere(eq(projects.id, id)) without orgId is a cross-tenant read. Scope by both.
  • Returning raw rows with secrets — strip sensitive columns (tokens, hashes) in the DAL.
  • N+1 in loops — batch with inArray or a join instead of awaiting per item.
  • Forgetting server-only — without it a refactor can pull DB code into the client bundle.

Hand-off

A typed, tenant-safe query layer. api-routes-and-validation and Server Actions call it after validating input; authorization-rbac gates which functions a role may invoke.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.