Next.js + Prisma で PostgreSQL の Row Level Security を試す

近年、バイブコーディングや個人開発の現場において、Next.js と Supabase を組み合わせたアプリケーション開発が急速に広がっています。

Supabase は、PostgreSQL を基盤とした BaaS(Backend as a Service)であり、認証やストレージ、データベース操作といった機能を短時間で導入できる点が魅力です。Next.js と併用することで、フロントからバックエンドまでを一気通貫で実装できるため、特に個人開発やスタートアップにとって非常に有用な選択肢となっています。

しかし一方で、この手軽さがセキュリティ上のリスクを生むケースも少なくありません。

特に懸念されるのは、Row Level Security(RLS)を適切に設定していないことによって、アプリケーションの利用者が他のユーザーのデータにアクセスできてしまう脆弱性です。実際、海外の開発者ブログやSNS上でも、Supabase を利用したプロジェクトで「認可設定が甘く、ユーザーデータが丸見えになっていた」といった事例が報告されています。これは単純な実装ミスであると同時に、「DB レイヤーでのアクセス制御を軽視した設計」が引き起こす典型的な問題でもあります。

アプリケーションコードの中で「where 句」を書き忘れたり、認証の条件分岐が抜けてしまったりすることは、人間がコードを書く以上どうしても起こり得ます。そうしたヒューマンエラーを補完し、データの安全性を保証するために有効なのが、PostgreSQL が備える Row Level Security(RLS) です。RLS は、テーブルごとに「誰がどの行を参照・更新できるのか」をポリシーとして定義でき、アプリケーション層のバグに左右されず、データベース側で強制的に境界を守ることができます。

本記事では、Supabase の文脈で話題に上がることの多い RLS を、より基盤寄りの構成(Next.js + Prisma + Docker Compose + PostgreSQL)で実際に構築し、その有効性を確認していきます。

認証セッションや JWT といった仕組みと組み合わせることで、開発規模が大きくなっても安全性を確保できる堅牢なアプリケーション設計が可能になります。

この記事を通して読者の方に伝えたいのは、「アプリ層だけでなくデータベース層でもセキュリティ境界を確立することの重要性」です。Next.js や Supabase を利用して個人開発やスタートアップ開発を進めている方にとっても、よりセキュアな設計を実践する上で参考となるはずです。

Row Level Security(RLS)とは

PostgreSQL が提供する Row Level Security(RLS) は、テーブルごとに行レベルでアクセス制御を行う仕組みです。通常はアプリケーション側で「WHERE 句」を付与してユーザーごとのデータ制限を実現しますが、この方法だとコードの書き漏らしやバグによって他人のデータにアクセスできてしまう可能性があります。RLS を使えば、データベース自身が行単位でアクセス制御を強制するため、アプリケーション層の不備を補完できるのが大きな特徴です。

どのバージョンから利用できるのか

RLS は PostgreSQL 9.5(2016年リリース) から導入されました。

その後、9.6 以降では細かな機能改善が続き、現在の最新バージョン(15, 16, 17 系列)でも標準機能として利用できます。

つまり、近年のほとんどの PostgreSQL 環境(Supabase や Cloud SQL などのマネージドサービスを含む)では、追加モジュールを導入することなくすぐに RLS を有効化できます。

仕組みの概要

  • 有効化 各テーブルごとに ENABLE ROW LEVEL SECURITY を指定すると RLS が有効になります。さらに FORCE ROW LEVEL SECURITY を付けることで、スーパーユーザーを除くすべてのクエリにポリシーが強制されます。
  • ポリシー定義 CREATE POLICY を使って「どの条件を満たす行を参照できるか/更新できるか」を定義します。 たとえば、company_id がセッション変数に一致する行だけを返すようにすれば、ユーザーは自分の会社のデータしか操作できなくなります。
  • 参照と更新の区別 ポリシーは USING(参照可能な行の条件)と WITH CHECK(挿入・更新できる行の条件)の二種類を持ちます。これにより、読み取りと書き込みの制御をきちんと分けて設定できます。

活用されるシーン

  • マルチテナント型のSaaS 1つのデータベースに複数企業のデータを格納する場合、RLS を使うことで「他社のデータを見られない」という保証をDB側で確実に実現できます。
  • 個人向けサービス 個別ユーザーごとに独立したデータを保持する場合、user_id 単位で RLS を設定すれば、本人以外はアクセスできません。
  • セキュリティ要件が厳しいシステム アプリ層のバグや抜け漏れがあっても、DB側で強制的に境界を守れることは監査や法令遵守の観点でも重要です。

なぜ注目されているのか

Supabase の普及によって、PostgreSQL 標準機能である RLS の存在が一般開発者にも広く知られるようになりました。しかし一方で、RLS を有効化していなかったりポリシーが適切でなかったために他ユーザーのデータが閲覧可能になる事故が報告されるケースも見られます。

このような背景から、個人開発やスタートアップ開発でも RLS を意識的に取り入れるべきという認識が高まっています。

動作確認の流れ

本記事で紹介するサンプルは、Next.js + Prisma + PostgreSQL(Docker Compose) という構成をベースにしています。ここでは細かいコードは割愛し、全体像を段階的に示します。

まず最初に、フロントエンドとバックエンドの統合的な実装基盤として Next.js プロジェクトを用意します。Next.js はフロントエンドフレームワークという印象が強いですが、Route Handlers や Server Actions を利用することで、バックエンド API を容易に組み込むことができます。今回は画面を構築せず、API サーバーとしての役割に集中させます。

次に、ORM として Prisma を導入します。Prisma を使うことで、データベース操作を型安全に行え、マイグレーションやクエリ管理も容易になります。Next.js との統合もしやすく、開発効率を高められる選択肢です。

データベースには PostgreSQL を採用し、ローカル環境では Docker Compose で起動します。コンテナを利用することで環境差異を減らし、CI/CD パイプラインでも再現しやすくなります。ここで重要なのは、アプリケーション接続用のデータベースユーザーを 非スーパーユーザー として作成することです。これにより、常に RLS が適用される安全な環境を構築できます。

環境が整ったら、Prisma のスキーマ定義を通じて company と user の2つのモデルを設計します。マイグレーションを実行することで実際のテーブルが作成され、RLS を適用できる状態が整います。

続いて、PostgreSQL 側で RLS を設定します。各テーブルに対して「どの会社に属するデータにアクセスできるか」をポリシーとして定義し、アプリケーション側からはセッション変数経由で company_id を渡します。これにより、アプリケーションコードの不備があってもデータベースが境界を守り続ける構成となります。

最後に、Next.js の Route Handlers で CRUD API を実装し、Postman などのツールを使って動作確認を行います。会社ごとに返却されるデータが異なることを確認できれば、RLS が正しく効いていることが証明されます。

ステップ一覧

1. Next.js プロジェクトの作成 → フロント兼バックエンドの基盤を用意
2. Prisma の導入と初期化 → ORM として採用し、DB操作の型安全性とマイグレーション管理を担保
3. Docker Compose による PostgreSQL の起動 → 非スーパーユーザー(NOBYPASSRLS付き)を用意し、安全な接続ユーザーを確保
4. Prisma スキーマの定義 → company と user モデルを記述し、マイグレーションでテーブルを生成
5. RLS の設定 → PostgreSQL 側にポリシーを定義し、行レベルでアクセス制御を強制
6. API 実装(Next.js Route Handlers) → CRUD API を構築し、セッション変数によって RLS を効かせる
7. 動作確認 → 会社ごとに返却データが異なることを確認し、RLS が有効であることを検証

プロジェクト構築と Prisma 導入

本記事では、Next.js をベースとしたプロジェクトに Prisma を導入し、PostgreSQL と接続できる状態を整えます。ここでは、実際のコマンドや設定コードを差し込む場所を示し、流れの全体像を整理していきます。

1. Next.js プロジェクトの新規作成

まずは Next.js プロジェクトを新規作成します。

ここで紹介するケースでは、画面部分は利用せず API 実装を中心とするため、Route Handlers を活用したバックエンド API サーバーとして Next.js を利用します。

> npx create-next-app@latest next-rls-prisma
[質問にはすべてデフォルトで回答]
> cd next-rls-prisma

2. Prisma の導入

次に、Prisma をプロジェクトに導入します。Prisma はモダンな ORM であり、型安全なクエリの提供やマイグレーション管理を通じて、開発効率と安全性を高めてくれます。

> npm i -D prisma
> npm i @prisma/client

3. Prisma の初期化

Prisma を導入したら、初期化を行います。この操作により.envファイルとprisma/schema.prismaファイルが生成されます。

.envは接続情報を定義する環境変数ファイル、schema.prismaはデータベーススキーマを記述する中心的な設定ファイルとなります。

> npx prisma init

ここまで完了すれば、Next.js プロジェクトと Prisma の接続準備が整い、次の章で行う Docker Compose による PostgreSQL の環境構築に進むことができます。

Docker Compose でデータベースを構築し、.env を設定する

Next.js プロジェクトと Prisma の準備ができたら、次はローカル環境で利用する PostgreSQL を Docker Compose を使って立ち上げます。コンテナを使うことで環境構築が容易になり、チーム開発や CI 環境でも再現性を担保できます。

本記事では、アプリケーション接続用に 非スーパーユーザー(RLS バイパス不可のユーザー) を作成するように初期化スクリプトを設定します。これにより、後のステップで RLS を適用した際に確実に効かせられる安全な環境を用意できます。

1. docker-compose 設定ファイルの用意

まずはcompose.yamlを作成し、PostgreSQL サービスを定義します。

ここでは、初期化スクリプトを配置するフォルダを指定しておくことで、アプリケーション用ユーザーを自動的に作成できるようにします。

services:
  db:
    image: postgres:17
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: password
      POSTGRES_DB: appdb
    ports:
      - "5432:5432"
    volumes:
      - ./initdb:/docker-entrypoint-initdb.d

2. 初期化スクリプトの配置

Docker 公式の PostgreSQL イメージは、/docker-entrypoint-initdb.d/ 配下に配置された SQL ファイルを初回起動時に実行してくれます。この仕組みを利用して、アプリケーション用のユーザー(例: app_rw)を作成し、必要な権限を与えます。

-- アプリ用:非superuser・RLSバイパス不可・migrate用にCREATEDBを付与
CREATE ROLE app_rw LOGIN PASSWORD 'app_rw_password'
  NOSUPERUSER NOBYPASSRLS CREATEDB;

-- publicスキーマの利用 + 作成を許可(← これが無いとテーブル作成できない)
GRANT USAGE, CREATE ON SCHEMA public TO app_rw;

-- 既存オブジェクトへの権限
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES    IN SCHEMA public TO app_rw;
GRANT USAGE, SELECT                  ON ALL SEQUENCES IN SCHEMA public TO app_rw;

-- これから「app_rw が作成する」オブジェクトに自動付与(明示しておく)
ALTER DEFAULT PRIVILEGES FOR ROLE app_rw IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_rw;

ALTER DEFAULT PRIVILEGES FOR ROLE app_rw IN SCHEMA public
  GRANT USAGE, SELECT ON SEQUENCES TO app_rw;

3. .env の設定変更

次に、Prisma が利用する .env の DATABASE_URL を、先ほど作成したアプリケーション用ユーザーで接続するように変更します。

DATABASE_URL="postgresql://app_rw:app_rw_password@localhost:5432/appdb?schema=public"

このステップを終えることで、Next.js + Prisma プロジェクトから PostgreSQL に接続可能な状態が整います。次の章からは、Prisma スキーマを編集し、実際にマイグレーションを実行してテーブルを作成していきます。

company / user モデルを追加し、マイグレーションを実行する

この章では、RLS をかける前段として company と user の2モデルを Prisma スキーマに追加します。テーブル/カラム名は運用で扱いやすい snake_case に統一し、主キーは cuid(ハイフンなしの文字列ID) を採用します。

1. Prisma スキーマにモデルを追加

companyモデルとuserモデルを定義します。

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

// 企業モデル
model company {
  id   String @id @default(cuid())
  name String

  users user[]
}

// ユーザーモデル
model user {
  id         String  @id @default(cuid())
  name       String
  company_id String
  company    company @relation(fields: [company_id], references: [id])
}

注意:Prisma を初期化したときに generator client に output 行が含まれていることがあります。これは削除してください。デフォルト設定を利用すれば Prisma Client は node_modules/.prisma/client に生成され、アプリ側からは import { PrismaClient } from “@prisma/client”; で問題なく利用できます。独自の出力先を指定すると環境ごとにパスがずれて不具合を起こすため、あえて残す理由がない限り削除するのが安全です。

2. マイグレーションを作成・適用

スキーマの変更をデータベースに反映します。

> npx prisma migrate dev --name init

マイグレーションを実行すると以下が行われます。

  • prisma/migrations/<timestamp>__init/ディレクトリが生成される
  • DB にcompany / userテーブルが作成される
  • Prisma Client が自動生成され、アプリから利用できる状態になる

注意:マイグレーション時には .env の DATABASE_URL が正しく app_rw(非スーパーユーザー、NOBYPASSRLS 付き、USAGE, CREATE ON SCHEMA public 権限あり)を指していることを確認してください。これが誤っていると「permission denied for schema public」などのエラーになります。

3. テーブル作成の確認

テーブルが作成されているかを確認します。Prisma Studioを使う方法が簡単です。

> npx prisma studio

これで RLS を適用できる土台(company / user テーブル) が整いました。

次の章では、PostgreSQL 側で RLS を有効化し、ポリシーを定義する手順に進みます。

RLS を適用するマイグレーションを追加する

この章では、すでに作成した company / user テーブルに対して Row Level Security(RLS) を有効化し、会社境界(company_id)でのデータ分離をポリシーとして設定します。以降、アプリケーションからはセッション変数で会社IDを注入することで、クエリに WHERE を書かずとも DB 側で行レベルの制御が強制されるようになります。

1. RLS 用のマイグレーション雛形を作る

RLS は Prisma のスキーマ記法では表現できないため、生の SQL を含むマイグレーションを作ります。まず “空の” マイグレーションを発行します。

> npx prisma migrate dev --name add-rls-user

これでprisma/migrations/<timestamp>__add-rls-user/migration.sqlが生成されます。

2. 生成されたマイグレーションスクリプトに RLS の SQL を追記

user テーブルに対して RLS を 有効化(ENABLE)強制(FORCE) し、company_id がセッション変数に一致する行のみ許可するポリシーを定義します。

セッション変数名は名前衝突を避けるため app.company_id のようにプレフィックスを付けるのが安全です。

-- UserテーブルにRLSを設定(会社境界で制限)
ALTER TABLE "user" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "user" FORCE ROW LEVEL SECURITY;

CREATE POLICY user_by_company ON "user"
  FOR ALL
  USING      (company_id = current_setting('app.company_id', true))
  WITH CHECK (company_id = current_setting('app.company_id', true));

3. マイグレーションを適用する

追記が終わったら、DB に適用します。

> npx prisma migrate dev

もしシャドーDB作成が必要な構成で、アプリ接続ユーザーに CREATEDB を付与していない場合は、schema.prisma の datasource に shadowDatabaseUrl を設定して superuser を使う運用にしておくと安定します(この章では設定コードは割愛、前章の方針どおりでOK)。

4. RLS が適用されたかを確認する

以下は psql から確認する手順です。アプリ接続用の 非スーパーユーザー(例: app_rw) で接続して実行してください。

4.1. 接続

# 例: docker compose で起動している場合
> docker compose exec -T db psql -U app_rw -d appdb

もしスーパーユーザーで入る場合は、各セッションで先に SET row_security = on; を実行してください(superuserは既定でRLSをバイパスするため)。

4.2. RLS の有効化・強制状態を確認

-- RLSフラグ(有効/強制)の確認
SELECT relname, relrowsecurity, relforcerowsecurity
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND relname = 'user';

-- 付与済みポリシーの確認
SELECT schemaname, tablename, policyname, cmd, qual, with_check
FROM pg_policies
WHERE schemaname = 'public' AND tablename = 'user';
  • relrowsecurity = t かつ relforcerowsecurity = t であること
  • user テーブルに company_id = current_setting(‘app.company_id’, true) を条件とするポリシーが載っていること

4.3. セッション変数なしだと行が見えないことを確認

-- セッション変数未設定の状態
SELECT * FROM "user";

期待:0 行(または権限エラー)。

理由:USING (company_id = current_setting(‘app.company_id’, true)) が満たせないため。

アプリ接続ユーザーは 非スーパーユーザー(NOBYPASSRLS) を使用してください。superuser で接続する場合は SET row_security = on; を入れないと RLS が適用されません(本番運用では非superuserが原則)。

4.4. つまづかないための事前注意(簡潔に)

  • テーブル・カラム名と SQL の表記を一致させる(snake_case で統一)。
  • FORCE を付けることで、所有者や誤設定によるバイパスを防ぐ。
  • セッション変数名に app. プレフィックスを付ける(カラム名と混同しないため)。
  • 非superuser + NOBYPASSRLS のアプリユーザーで接続する(compose の init スクリプトで作成済み想定)。

バックエンド API を作る(PrismaClient 準備 → CRUD 実装)

RLS を効かせるために、API から DB へアクセスする際は トランザクション先頭で set_config(‘app.company_id’, …) を実行する方針にします。今回は検証しやすいように、認証の代わりに x-company-id ヘッダで会社IDを受け取り、その値を set_config に渡します(※本番ではセッション/JWTから注入)。

1. PrismaClient の作成(共通モジュール)

Next.js から Prisma を再利用できるよう、シングルトンの PrismaClient を用意します。

  • ファイル:/lib/prisma.ts
  • 目的:開発中のホットリロードで複数インスタンスが出来ないようにする。ログ設定などもここで。
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === "development" ? ["query", "warn", "error"] : ["error"],
  });

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

2. API 仕様の方針

  • ベースURL:/api
  • リソース:/companies(管理用:RLSなし), /users(RLS対象)
  • テナント切り替え:x-company-id ヘッダ(users系のみ必須
  • 例外方針:RLSで見えない行は 404 と等価の扱いにする(更新/削除も同様)

3. ディレクトリ構成

app/
  api/
    companies/
      route.ts        # GET(list), POST(create)
      [id]/
        route.ts      # GET(read), PATCH(update), DELETE(delete)
    users/
      route.ts        # GET(list), POST(create)  ← RLS適用(要ヘッダ)
      [id]/
        route.ts      # GET, PATCH, DELETE       ← RLS適用(要ヘッダ)
lib/
  prisma.ts

4. Companies API(管理用:RLSなし)

4.1. 一覧 & 作成

  • ファイル:app/api/companies/route.ts
  • ハンドラ
    • GET /api/companies?skip=0&take=50(ページング)
    • POST /api/companies(body: { name: string })
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

// GET /api/companies?skip=0&take=50
export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const skip = Number(searchParams.get("skip") ?? "0");
  const take = Math.min(Number(searchParams.get("take") ?? "50"), 200);

  const [items, total] = await Promise.all([
    prisma.company.findMany({ skip, take, orderBy: { name: "asc" } }),
    prisma.company.count(),
  ]);

  return NextResponse.json({ items, total, skip, take });
}

// POST /api/companies  body: { name: string }
export async function POST(req: NextRequest) {
  const body = await req.json().catch(() => null) as { name?: string } | null;
  if (!body?.name) {
    return NextResponse.json({ error: "name is required" }, { status: 400 });
  }

  const company = await prisma.company.create({ data: { name: body.name } });
  return NextResponse.json(company, { status: 201 });
}

4.2. 参照・更新・削除

  • ファイル:app/api/companies/[id]/route.ts
  • ハンドラ
    • GET /api/companies/:id
    • PATCH /api/companies/:id(body: { name?: string })
    • DELETE /api/companies/:id
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function GET(
  _req: NextRequest,
  { params }: { params: { id: string } }
) {
  const company = await prisma.company.findUnique({ where: { id: params.id } });
  if (!company) return NextResponse.json({ error: "Not found" }, { status: 404 });
  return NextResponse.json(company);
}

export async function PATCH(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  const body = await req.json().catch(() => null) as { name?: string } | null;
  if (!body) return NextResponse.json({ error: "invalid json" }, { status: 400 });

  try {
    const updated = await prisma.company.update({
      where: { id: params.id },
      data: { ...(body.name ? { name: body.name } : {}) },
    });
    return NextResponse.json(updated);
  } catch {
    return NextResponse.json({ error: "Not found" }, { status: 404 });
  }
}

export async function DELETE(
  _req: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    await prisma.company.delete({ where: { id: params.id } });
    return NextResponse.json({ ok: true });
  } catch {
    return NextResponse.json({ error: "Not found" }, { status: 404 });
  }
}

5. Users API(RLS対象:x-company-id 必須)

5.1. 一覧 & 作成

  • ファイル:app/api/users/route.ts
  • ヘッダ:x-company-id: <company_id>(必須)
  • ハンドラ
    • GET /api/users?skip=0&take=50
      1. ヘッダ検証 → 2) $transaction 開始 → 3) set_config(‘app.company_id’, companyId, true) → 4) findMany と count
    • POST /api/users(body: { name: string })
      1. ヘッダ検証 → 2) $transaction + set_config → 3) create({ data: { name, company_id: companyId } }) ※ WITH CHECK が効くため、万一クライアントが別の company_id を送っても DB が拒否
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

// GET /api/users?skip=0&take=50
export async function GET(req: NextRequest) {
  const companyId = req.headers.get("x-company-id");
  if (!companyId) {
    return NextResponse.json({ error: "x-company-id header required" }, { status: 400 });
  }

  return prisma.$transaction(async (tx) => {
    await tx.$executeRaw`select set_config('app.company_id', ${companyId}, true)`;

    const { searchParams } = new URL(req.url);
    const skip = Number(searchParams.get("skip") ?? "0");
    const take = Math.min(Number(searchParams.get("take") ?? "50"), 200);

    const [items, total] = await Promise.all([
      tx.user.findMany({ skip, take, orderBy: { name: "asc" } }),
      // RLS が効くので count も自動で同じ境界に制限される
      tx.user.count(),
    ]);

    return NextResponse.json({ items, total, skip, take });
  });
}

// POST /api/users  body: { name: string, company_id?: string }
export async function POST(req: NextRequest) {
  const companyId = req.headers.get("x-company-id");
  if (!companyId) {
    return NextResponse.json({ error: "x-company-id header required" }, { status: 400 });
  }

  const body = await req.json().catch(() => null) as { name?: string; company_id?: string } | null;
  if (!body?.name) {
    return NextResponse.json({ error: "name is required" }, { status: 400 });
  }

  return prisma.$transaction(async (tx) => {
    await tx.$executeRaw`select set_config('app.company_id', ${companyId}, true)`;

    // 安全のため、API入力の company_id は無視してサーバ側で上書き
    const created = await tx.user.create({
      data: { name: body.name, company_id: companyId },
    });

    return NextResponse.json(created, { status: 201 });
  });
}

5.2. 参照・更新・削除

  • ファイル:app/api/users/[id]/route.ts
  • ハンドラ
    • GET /api/users/:id → $transaction + set_config → findUnique。RLSにより他社IDは見えない=404相当
    • PATCH /api/users/:id(body: { name?: string }) → $transaction + set_config → update。RLS条件を満たさないと対象0件=404
    • DELETE /api/users/:id → $transaction + set_config → delete。同上
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

// GET /api/users/:id
export async function GET(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  const companyId = req.headers.get("x-company-id");
  if (!companyId) {
    return NextResponse.json({ error: "x-company-id header required" }, { status: 400 });
  }

  return prisma.$transaction(async (tx) => {
    await tx.$executeRaw`select set_config('app.company_id', ${companyId}, true)`;

    const user = await tx.user.findUnique({ where: { id: params.id } });
    if (!user) return NextResponse.json({ error: "Not found" }, { status: 404 });
    return NextResponse.json(user);
  });
}

// PATCH /api/users/:id  body: { name?: string }
export async function PATCH(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  const companyId = req.headers.get("x-company-id");
  if (!companyId) {
    return NextResponse.json({ error: "x-company-id header required" }, { status: 400 });
  }
  const body = await req.json().catch(() => null) as { name?: string } | null;
  if (!body) return NextResponse.json({ error: "invalid json" }, { status: 400 });

  return prisma.$transaction(async (tx) => {
    await tx.$executeRaw`select set_config('app.company_id', ${companyId}, true)`;

    try {
      const updated = await tx.user.update({
        where: { id: params.id },
        data: { ...(body.name ? { name: body.name } : {}) },
      });
      return NextResponse.json(updated);
    } catch {
      // RLSに弾かれた or 存在しない
      return NextResponse.json({ error: "Not found" }, { status: 404 });
    }
  });
}

// DELETE /api/users/:id
export async function DELETE(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  const companyId = req.headers.get("x-company-id");
  if (!companyId) {
    return NextResponse.json({ error: "x-company-id header required" }, { status: 400 });
  }

  return prisma.$transaction(async (tx) => {
    await tx.$executeRaw`select set_config('app.company_id', ${companyId}, true)`;

    try {
      await tx.user.delete({ where: { id: params.id } });
      return NextResponse.json({ ok: true });
    } catch {
      return NextResponse.json({ error: "Not found" }, { status: 404 });
    }
  });
}

6. 動作確認

会社を2つ作成します。

POST http://localhost:3000/api/companies
Body: { "name": "Acme" }
→ id をメモ
POST http://localhost:3000/api/companies
Body: { "name": "Globex" }
→ id をメモ

次にそれぞれの会社にユーザーを作成します。

POST http://localhost:3000/api/users
Headers: x-company-id: <Acme社のid>
Body: { "name": "Alice" }
→ 201 Created
POST http://localhost:3000/api/users
Headers: x-company-id: <Globex社のid>
Body: { "name": "Bob" }
→ 201 Created

それぞれのユーザー一覧が取得できることを確認します。

GET http://localhost:3000/api/users
Headers: x-company-id: <Acme社のid>
→ [ { name: "Alice", company_id: <Acme社のid> } ] のみ取得できることを確認
GET http://localhost:3000/api/users
Headers: x-company-id: <Globex社のid>
→ [ { name: "Bob", company_id: <Globex社のid> } ] のみ取得できることを確認

最後に、ユーザーと企業が一致しないケースではデータが取得できないことを確認します。

GET http://localhost:3000/api/users/<Aliceのid>
Headers: x-company-id: <Acme社のid>
→ [ { name: "Alice", company_id: <Acme社のid> } ] のみ取得できることを確認
GET http://localhost:3000/api/users/<Aliceのid>
Headers: x-company-id: <Globex社のid>
→ 404 Not Foundになることを確認

7. 実際に使用する際のメモ

  • x-company-id はデモ用。本番は認証セッション/JWTから company_id を取得
  • 管理者ロールを導入する場合は set_config(‘app.is_admin’,’true’,true) を追加し、RLSポリシーに OR 条件を拡張

まとめ

本記事では、PostgreSQL の Row Level Security(RLS)を Next.js + Prisma 環境で適用する方法を、一から順を追って解説しました。

まず、RLS とは何か、その背景やどのバージョンから利用できるのかといった基礎知識を整理し、データベース側で強制的に行レベルのアクセス制御を行う重要性を確認しました。続いて、Next.js プロジェクトを新規作成し、Prisma を導入してローカル環境に PostgreSQL を Docker Compose で構築しました。さらに、company / user モデルを設計し、マイグレーションによって実際のテーブルを作成。その上で、RLS を有効化してポリシーを設定し、会社単位でデータが分離される仕組みを確認しました。

最後に、PrismaClient を使って Next.js の Route Handlers に CRUD API を実装し、x-company-id ヘッダを通じてセッション変数を注入することで、アプリケーション層の記述に依存せず DB 側で安全に境界を守る仕組みを完成させました。Postman での検証を通じて、会社ごとに結果が切り替わることや、他社データにはアクセスできないことを確認できました。

RLS は アプリ層のミスをデータベース層でカバーできる強力な仕組みです。とりわけマルチテナントの SaaS やセキュリティ要件の高いサービスでは、導入する価値が非常に大きいといえます。Supabase を利用する個人開発でも、Next.js + Prisma を利用するチーム開発でも、「RLS を前提とした設計」を意識することが、今後ますます重要になるでしょう。

これから RLS を試してみようと考えている方は、ぜひ本記事の流れを参考にして、まずはローカル環境で小さなサンプルを動かすところから始めてみてください。

参考文献

アシスト、日本でELTツール「Fivetran」の販売を開始──他の国内パートナー企業との違いとは?

はじめに

2025年7月15日、株式会社アシストは、米Fivetran Inc.が提供するクラウド型ELT(Extract-Load-Transform)ツール「Fivetran」の日本国内販売を正式に開始しました。これにより、従来は一部パートナーを通じて提供されていたFivetranが、より広範な企業へと導入しやすくなり、国産企業のデータ基盤整備のハードルが一段と下がることが期待されます。

Fivetranは、数百を超えるクラウドサービスやデータベースと連携でき、複雑なデータ変換処理を排し、ノーコードで高速・安定したデータパイプラインを構築できるのが特徴です。データの「抽出→格納→変換」というELT方式を採用し、クラウドDWH(Snowflake、BigQuery、Redshiftなど)の能力を最大限に活用する設計がなされています。

今回の発表で特に注目されているのは、アシストが「2025 Fivetran Global Partner Awards」においてAPAC地域の「Rising Star Partner of the Year」に選出されたという点です。これは、同社が持つBIやDWH、データ統合の支援実績が世界的に評価された証でもあり、国内パートナーの中でも一歩リードしたポジションにあることを示しています。

この記事では、Fivetranとはどのような製品なのかを改めて整理しつつ、アシストをはじめとする国内の主な導入支援パートナーの特徴を比較し、導入を検討する際に考慮すべきポイントを紹介していきます。

Fivetranとは?

Fivetran(ファイブトラン)は、米カリフォルニア州オークランドに本社を構えるFivetran Inc.が提供する、フルマネージド型のクラウドELTプラットフォームです。特に、データ統合の「複雑さ」を極限まで削減し、誰でも簡単に、安定したデータパイプラインを構築できることをコンセプトに開発されています。

🔄 ETLとの違い:なぜ“ELT”なのか?

従来のデータ統合は「ETL」(Extract → Transform → Load)という流れで、変換処理をETLツール内で実行する構成が一般的でした。しかし、Fivetranはそれとは異なり、「ELT」(Extract → Load → Transform)を採用しています。これは、データの変換処理をDWH(データウェアハウス)側に任せることで、スケーラビリティとメンテナンス性を向上させ、パフォーマンスの最大化を実現するアプローチです。

🔌 圧倒的な対応コネクタ数とノーコード設定

Fivetranの大きな強みの一つが、700種類以上のコネクタに対応している点です。Salesforce、Google Analytics、Stripe、HubSpot、MySQL、PostgreSQL、MongoDB、さらにはSaaSやオンプレミスDB、ファイルストレージなど、ビジネスで利用されるあらゆるデータソースとの接続が可能です。

また、これらのコネクタはノーコードで接続・設定が可能で、専門的な知識がなくても数クリックで同期設定を完了できます。これにより、これまでデータエンジニアに頼らざるを得なかった業務を、現場のビジネス担当者でも一部担えるようになる点が画期的です。

🔁 自動同期・差分更新・スキーマ変化への追従

Fivetranは、初回同期以降は差分のみを検出して自動で取り込む設計になっており、DWHへの負荷を最小限に抑えながら、常に最新状態のデータを保持することが可能です。さらに、データソース側でのスキーマ変更(列の追加・削除・型変更など)にも自動で追従できるため、保守運用コストが格段に低減されます。

💡 こんな課題を抱える企業に最適

Fivetranは以下のような課題を抱える企業にとって特に有効です:

  • 「データソースが多すぎて手作業で連携するのは非現実的」
  • 「データパイプラインが属人化しており、保守が困難」
  • 「マーケや営業部門が自分たちでデータを活用したい」
  • 「DWHやBIは導入したが、データの整備が追いつかない」

導入によって、データ分析基盤の整備やDX(デジタルトランスフォーメーション)推進を加速する土台が構築されます。

アシストによるFivetran販売の背景

株式会社アシストは、2025年7月15日にFivetran Inc.との正式な代理店契約を締結し、日本国内でのFivetranの提供と導入支援を本格的に開始しました。この発表は、単なる製品販売の開始という枠を超え、日本市場におけるFivetranの本格普及が始まったことを象徴する出来事として注目されています。

アシストはこれまで、企業のBI(ビジネスインテリジェンス)やDWH(データウェアハウス)構築を多数手がけてきた老舗のITサービス企業であり、特に「データをどう活かすか」「データ活用の基盤をどう整えるか」に関して豊富なノウハウを持っています。既存のETL製品やデータ連携基盤を多数扱ってきた経験がある同社にとって、Fivetranはその“次世代ソリューション”として位置づけられています。

さらに、アシストは今回の取り組みに先立ち、社内でのPoC(概念実証)や一部顧客との先行プロジェクトを通じて、Fivetranの国内環境における有効性や適応可能性を丁寧に検証してきました。その結果、以下のような判断に至ったとされています:

  • ノーコードであるため、開発リソースを割かずに導入できる
  • スキーマ追従などの自動化機能が非常に高く、保守工数が激減する
  • クラウドDWHとの連携が優れており、パフォーマンス最適化がしやすい
  • 国内企業の「データ活用の初手」として適している

こうした実績と取り組みが評価され、アシストは「2025 Fivetran Global Partner Awards」にて、APAC(アジア太平洋地域)における“Rising Star Partner of the Year”に選出されました。この賞は、Fivetran本社がグローバルで特に注目した成長パートナーに与えるもので、日本国内企業として唯一の受賞となります。

この受賞は、Fivetranの導入を単なる“製品販売”で終わらせるのではなく、「データ活用の成功」まで導く支援ができるパートナーであることを裏付けています。

アシストは今後、Fivetranを単体で提供するだけでなく、同社の他の製品群(MotionBoard、DataSpider、Snowflakeなど)と組み合わせて、統合的なデータ活用基盤の構築を提案していく方針を打ち出しています。

国内の他パートナー企業

Fivetranは、日本国内では以前から一部の技術パートナーやSaaSインテグレーターを通じて導入されてきました。今回のアシストによる正式販売開始は注目を集めていますが、Fivetranの国内展開はそれ以前から水面下で進んでおり、既に導入実績を持つ企業も存在します。

本セクションでは、代表的な2社──クラスメソッドおよび日立ソリューションズ東日本──に焦点をあて、それぞれの特徴や支援スタイルの違いを紹介します。

🏢 クラスメソッド

クラスメソッドは、AWSや各種クラウドサービスに精通したクラウドネイティブ型のインテグレーターであり、早期からFivetranの導入支援に取り組んできた実績を持つ企業の1つです。

特に注目すべきは、Fivetranを単なるETL/ELTツールとしてではなく、「クラウドDWHと連携した分析基盤の中核」として位置づけている点です。AWSのAmazon RedshiftやGoogle BigQuery、Snowflakeなどと組み合わせたアーキテクチャ提案を数多く行っており、導入から運用、可視化ツール(Looker、Tableauなど)までトータルに支援する体制を整えています。

また、同社は技術ブログの発信力にも定評があり、Fivetranに関する実践的な設定ノウハウやユースケース、料金試算などを日本語で多数公開しています。これにより、ユーザー側でも事前に技術的なイメージを掴みやすいというメリットがあります。

クラスメソッドは、特に次のようなニーズを持つ企業に適しています:

  • 自社でAWSやGCPを活用している
  • クラウド移行とデータ活用を同時に進めたい
  • 内製チームによるデータ分析を加速させたい

🏢 日立ソリューションズ東日本

一方、日立ソリューションズ東日本は、大企業向けシステム構築や業務システム連携に強みを持つSIerです。2023年12月にはFivetran Inc.とのパートナー契約を締結し、日本市場におけるFivetranの展開を早期から支援しています。

同社の特徴は、Fivetranを使ったデータ連携を、ERPや基幹システムと組み合わせて提案している点にあります。SAPやOracleなどのエンタープライズ向けシステムからデータを抽出し、クラウドDWHに連携させる際の障壁を乗り越えるための設計力や、セキュリティ・ガバナンスへの配慮など、大規模企業が求める要件を満たす体制を有しています。

また、PoCから運用保守までを一貫して提供できる体制を持ち、特に「Fivetranを業務にどう組み込むか」に関する提案力が評価されています。

日立ソリューションズ東日本は、以下のようなニーズにマッチします:

  • ERPや業務系システムとクラウドを連携させたい
  • SIerによる一括導入・保守体制を求めている
  • グループ会社全体のデータ統合を進めたい

💡 まとめ:国内パートナーの選び方

このように、Fivetranの国内導入支援を行っているパートナー企業は、それぞれ異なる得意領域と支援スタイルを持っています。以下に簡潔に整理します。

企業名主な特徴対応領域
アシストBI・DWH・ELT連携の一体提案、グローバル賞受賞汎用・中堅〜大企業向け
クラスメソッドAWS/GCP対応、技術ブログ・ドキュメントが充実クラウドネイティブ型、内製推進型
日立ソリューションズ東日本基幹システム連携、大企業向け導入支援ERP/オンプレ連携、堅牢性重視

パートナー選定においては、単に「販売しているかどうか」だけでなく、自社のシステム環境・運用方針・内製志向かどうかといった観点での比較が重要となります。

今後の展望

Fivetranの国内展開は、アシストによる正式販売を契機に新たなフェーズに入ったといえます。従来は、クラウドやデータ基盤に強い一部の企業に限られていたFivetranの導入が、より幅広い業種・企業規模に拡大していくことが期待されます。

その背景には、企業が直面している共通課題があります。それは、「データはあるが、使える状態になっていない」という現実です。複数の業務システム、SaaS、外部サービスに分散したデータを“分析可能な状態”に集約するには、多くのコストと時間がかかります。しかも、その処理は属人化しやすく、メンテナンスの負担も大きくなりがちです。

こうした課題をFivetranは、ノーコード・自動化・高い保守性というアプローチで解決しようとしています。特に以下のようなトレンドと親和性が高く、今後の活用範囲はさらに広がると見られます。

✅ 1. 生成AI・LLMのための「クリーンな学習データ基盤」

多くの企業が生成AIや大規模言語モデル(LLM)を自社活用しようとする中で、最初の壁になるのが「学習・推論に適したデータの整備」です。Fivetranは、社内外のデータをリアルタイムで一元化し、変換ロジックもDWH上で制御可能なため、信頼できる学習データパイプラインを構築するうえで大きな武器になります。

✅ 2. 中堅・中小企業への展開拡大

これまでクラウドDWHやELT基盤は、大企業向けのソリューションという印象が強くありました。しかし、Fivetranは初期導入のハードルが低く、サブスクリプション型でスモールスタートが可能なため、中堅・中小企業にとっても導入現実性の高い選択肢です。

加えて、クラスメソッドのようなインテグレーターが中小規模案件を数多く手がけており、アシストもその層に向けた支援メニューを展開していく可能性があります。

✅ 3. 国産SaaS・業務アプリとの統合強化

Fivetranはもともと海外SaaSとの連携に強みがありますが、日本市場においては、kintone、サイボウズ、弥生、freee、マネーフォワードなどの国産業務サービスとの連携が、今後の大きなテーマになります。こうした領域では、パートナー企業によるコネクタ拡張APIブリッジの開発が進みつつあり、国内特化型のソリューションが生まれる可能性もあります。

✅ 4. ベンダーロックインの回避とマルチクラウド対応

Fivetranはベンダー中立的な立場をとっており、AWS、Azure、GCPなど複数のクラウド環境に対応しています。今後、企業のデータインフラがマルチクラウド/ハイブリッド構成になる中で、柔軟なデータ連携基盤としてFivetranが選ばれる機会はますます増えるでしょう。

また、クラウドからオンプレミス、SaaSからDWH、BIまでを一気通貫でつなぐ「統合データ基盤」を構築する際にも、中心的な役割を担うことが期待されています。

おわりに

Fivetranは、これまで煩雑で属人化しがちだったデータ統合の作業を、“誰でも・簡単に・自動で”行えるようにする革新的なプラットフォームです。ビジネスの現場では、あらゆる部門・サービスから日々大量のデータが生まれていますが、それらをリアルタイムで結び付け、意思決定や改善に役立てるためには、高速かつ信頼性の高いデータパイプラインが必要です。

そうした中、アシストが日本市場においてFivetranの正式な販売を開始した意義は非常に大きいと言えるでしょう。単に製品を届けるだけでなく、長年培ったBI・DWH・ETLのノウハウを活かし、導入から定着まで伴走する体制を整えている点は、他のツールやベンダーとは一線を画しています。

また、クラスメソッドや日立ソリューションズ東日本といった他の先行パートナー企業も、すでに実践的な知見を蓄積しており、Fivetranを軸としたデータ基盤構築は、今後さらに広範な業界・業種で普及していくことが見込まれます。

しかし、Fivetranを導入することがゴールではありません。本当の意味で価値を引き出すには、

  • 統合すべきデータは何か?
  • 誰がどのように活用するのか?
  • どんな指標を見て、どんな判断につなげるのか?

といった「データ活用の目的と文脈」を明確にする必要があります。Fivetranは、その土台をつくるための“最強の裏方”であり、組織のデータドリブン化を支えるインフラです。

これからFivetranの導入を検討する企業にとって重要なのは、ツール選定だけでなく、「誰と組むか」=パートナー選びです。自社の課題や技術力、予算に応じて、最適な支援者を選ぶことが、プロジェクト成功の鍵を握ります。

📚 参考文献

モバイルバージョンを終了