社内システムを内製すると、必ずどこかで「部長は全部見られるけど、パートさんは自分の入力だけ」という話になります。要件定義では一行で済むのに、実装すると急に厄介になる。しかも厄介さの正体は技術ではなく、役割の切り方を決めきれていないことにあります。
先に、この記事の要点をまとめます。
- 権限設計は「画面の出し分け」ではなく「データに触れる関数の入口」で決める。ボタンを隠すのは体験の話で、守りの話ではありません
- Next.js 16 では
middleware.ts が proxy.ts に改称され、非推奨になりました。Proxy は入口の交通整理であって、認可の最終防衛線として設計してはいけません(公式も同じ立場です)
- 役割は3〜5個から始める。最初から細かい権限マトリクスを組むと、運用で誰もメンテしなくなります
社内システムの権限まわりは、後から足すと作り直しになりやすい部分の代表格です。ただし1つだけ、「役割を増やしすぎると破綻する」とは逆方向の落とし穴があり、これは記事の後半で触れます。
この記事では、中小企業の社内システムを Next.js フルスクラッチで作る前提で、権限設計の考え方・実装の置き場所・費用感・失敗例までを、2026年7月時点の公式仕様に沿って整理します。
権限設計は誰の仕事?──要件定義で決めるべき3つのこと
権限設計は開発者ではなく、業務側が決める仕事です。技術者が決められるのは「どこでチェックするか」だけで、「誰が何をしてよいか」は現場の業務ルールそのものだからです。
受託の現場でいちばん手戻りが出るのが、ここの曖昧さです。「一般ユーザーと管理者があればいいです」でスタートし、テスト段階になって「経理は他部署の申請も見られないと困る」「パートさんに単価は見せられない」と噴出する。実装は動いているのに、データモデルからやり直しになります。
要件定義の段階で、最低限この3つを業務側と詰めておくと後半が楽になります。
1. 役割(ロール)の一覧と、その定義文
「管理者」「一般」ではなく、「システム管理者=アカウント発行と全データ閲覧ができる人。情シス担当1名」のように、役職名と実在の担当者数まで書くのがコツです。人数を書くと「この役割、実は1人しかいないから分ける意味がない」といった統廃合の判断ができます。
2. データの見える範囲(スコープ)
権限は「操作の種類」だけでは決まりません。同じ「閲覧できる」でも、全社分なのか、自部署分なのか、自分が入力した分だけなのかで実装がまるで違います。この「範囲」を操作と別軸で書き出すのが重要です。
3. 見せてはいけない項目
行単位ではなく列単位の制御です。「見積書は営業全員が見られるが、原価列は課長以上だけ」というやつです。これがあるかないかで、後述する DTO(データ転送オブジェクト)の設計が変わります。
この3点が固まっていれば、あとはコードに落とすだけの作業になります。逆にここが「追って決めます」のまま実装に入ると、権限チェックがコードのあちこちに散らばって、誰も全体像を把握できないシステムができあがります。御社で今まさに内製を検討中なら、まずこの3点を紙に書けるかを確かめてみてください。
RBACとABAC、中小企業はどちらを選ぶべき?
中小企業の社内システムなら、まず RBAC(役割ベース)で始めるのが現実的です。ABAC(属性ベース)は柔軟ですが、ルールを書ける人が社内にいないと運用が止まります。
権限管理の方式は、大きく2つに整理できます。
| RBAC(役割ベース) | ABAC(属性ベース) |
|---|
| 判定の仕方 | 「この人は課長だから、承認できる」 | 「申請額が10万円未満かつ同部署なら、承認できる」 |
| 設計のしやすさ | 役割一覧を作るだけ。業務側も理解できる | 条件式の設計が必要。専門知識がいる |
| 柔軟さ | 例外対応が苦手(役割が増えがち) | 例外を条件式で吸収できる |
| 運用の負荷 | 低い。人事異動時に役割を付け替えるだけ | 高い。ルールの棚卸しが必要 |
| 向く規模 | 数十〜数百ユーザーの社内システム | 大規模SaaS、複雑な取引条件を持つ業務 |
社員数十名〜200名程度の会社で、社内の業務システムを内製する。この前提なら、RBAC で十分に足ります。役割を3〜5個定義し、それぞれが「どの操作を」「どの範囲で」できるかを表にする。それだけで、実務上の要求の8割は満たせます。
ただし現実には、RBAC だけで割り切れないケースが必ず出ます。よくあるのが「自分が作成した申請だけは、一般ユーザーでも編集できる」という所有者ルールです。これは役割では表現できず、「そのレコードの作成者IDとログインユーザーIDが一致するか」という属性の判定になります。
私たちが受託で組むときは、RBACを土台にして、所有者判定だけを例外的に足すという折衷案をよく採ります。ABAC のフル装備は入れず、isOwner() という関数を1つ用意して、必要な箇所だけで併用する。これなら業務側も「役割+自分の分だけ」という説明で理解できますし、条件式の管理コストも発生しません。
御社の場合、「役割で説明しきれない例外」が3つ以内なら RBAC 寄せで問題ありません。5つ、10と増えるようなら、業務ルール自体を見直したほうが早い可能性があります。
Next.js 16でどこに権限チェックを置くべき?
認可チェックは Proxy(旧middleware)ではなく、データに触れる関数の入口に置きます。Proxy は「未ログインなら/loginへ飛ばす」程度の交通整理に留めるのが公式の推奨です。
ここが2026年時点でいちばん誤解されている部分なので、丁寧に整理します。
Next.js 16 で middleware.ts は proxy.ts に改称された
まず事実確認です。Next.js 16.0.0 で、middleware というファイル規約は非推奨(deprecated)となり、proxy に改称されました。同時に、Proxy は Node.js ランタイムがデフォルトになっています(出典: Next.js 公式 proxy.js リファレンス)。
公式は改称の理由をこう説明しています。「middleware」という語が Express.js のミドルウェアと混同されやすく、本来の用途を誤解させる。Proxy はアプリの前段にあるネットワーク境界であり、その性質を表す名前にした、と。さらに踏み込んで、「Middleware への依存は、他に手段がない場合を除いて避けることを推奨する」 とまで書かれています。
移行にはコードモッドが用意されています。
npx @next/codemod@canary middleware-to-proxy .
ファイル名と関数名(middleware → proxy)を機械的に置き換えるだけなので、既存プロジェクトの移行コスト自体は軽微です。問題は名前ではなく、「middleware に認可を全部書いていた設計」がそのまま残ることです。
なぜ Proxy を認可の最終防衛線にしてはいけないのか
理由は3つあります。
1つめ、公式ドキュメントが明確に否定している。 Next.js の認証ガイドは、Proxy での権限チェックを「オプティミスティック(楽観的)チェック」と位置づけています。用途は「UIの出し分け」「未ログインユーザーの事前フィルタ」であり、こう書かれています。「Proxy は初期チェックには有用だが、データを守る唯一の防衛線であってはならない。セキュリティチェックの大半は、可能な限りデータソースの近くで行うべきである」(出典: Next.js 公式 認証ガイド)。
2つめ、Server Function(Server Actions)は Proxy のマッチャーから漏れうる。 公式リファレンスに明記されている、実務上かなり怖い挙動です。Server Function は独立したルートではなく、それが使われているルートへの POST として処理されます。つまりProxy のマッチャー設定でそのパスを除外していると、Server Function の呼び出しも Proxy を通りません。さらに、マッチャーの変更や、Server Function を別ルートへ移動するリファクタリングだけで、Proxy の保護が静かに外れることがあります。公式も「Proxy に頼らず、各 Server Function の内部で認証・認可を検証すること」を推奨しています。
3つめ、過去に実際に迂回された。 2025年に公開された CVE-2025-29927 は、x-middleware-subrequest ヘッダーをフレームワークが無検証で信頼していたため、攻撃者がこのヘッダーを偽装するだけで middleware 層を丸ごとスキップできた、というものです。CVSS 9.1 のクリティカル評価で、影響を受けるのは 12.3.5 / 13.5.9 / 14.2.25 / 15.2.3 より前のバージョンでした(出典: Snyk による解説、JFrog による解説)。
現行バージョンではもちろん修正済みですが、教訓は「特定のバグが直った」ではありません。入口の1枚に全部の責任を負わせる設計は、その1枚が破られたときに全部が破られるという構造の問題です。

実装の置き場所──Data Access Layer に集約する
では、どこに置くか。公式が推奨しているのが DAL(Data Access Layer) です。データ取得・更新の関数を server-only なモジュールに閉じ込め、その中で「セッション検証 → 認可チェック → クエリ → 必要な列だけ返す」を一気通貫でやります。
最小構成はこうなります。
// app/lib/dal.ts
import 'server-only'
import { cache } from 'react'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { decrypt } from '@/app/lib/session'
export const verifySession = cache(async () => {
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
if (!session?.userId) {
redirect('/login')
}
return { isAuth: true, userId: session.userId, role: session.role }
})
cache() で包むのは、1回のレンダリングの中で何度呼ばれても検証が1回で済むようにするためです。ページ・レイアウト・末端コンポーネントのどこから呼んでも安全に使えます。
そのうえで、役割の判定をここに1本化します。
// app/lib/authz.ts
import 'server-only'
import { verifySession } from '@/app/lib/dal'
type Action = 'estimate:read' | 'estimate:write' | 'estimate:approve' | 'cost:read'
const ROLE_PERMISSIONS: Record<string, Action[]> = {
admin: ['estimate:read', 'estimate:write', 'estimate:approve', 'cost:read'],
manager: ['estimate:read', 'estimate:write', 'estimate:approve', 'cost:read'],
sales: ['estimate:read', 'estimate:write'],
viewer: ['estimate:read'],
}
export async function authorize(action: Action) {
const session = await verifySession()
const allowed = ROLE_PERMISSIONS[session.role] ?? []
if (!allowed.includes(action)) {
throw new Error('Forbidden')
}
return session
}
ポイントは、権限を役割名ではなく「操作名」で表現していることです。呼び出し側に if (role === 'admin') を書かせない。こう書けるようになります。
// app/lib/actions.ts
'use server'
import { authorize } from '@/app/lib/authz'
export async function approveEstimate(estimateId: string) {
await authorize('estimate:approve') // ← 冒頭の1行がすべて
// 承認処理
}
なぜこれが重要かというと、運用で役割が増えたときに、修正箇所が ROLE_PERMISSIONS の1テーブルで済むからです。「役職名で分岐」を各所に書いていると、役割が1つ増えるたびに全ファイルを grep して回ることになります。人事異動や組織変更のたびに開発が発生する、という状態を避けるための設計です。
列単位の制御は DTO で行う
要件定義で挙げた「原価列は課長以上だけ」の話です。これは行を取ってきてから画面で隠すのでは不十分で、そもそもサーバーから返さないのが正解です。
// app/lib/dto.ts
import 'server-only'
import { verifySession } from '@/app/lib/dal'
export async function getEstimateDTO(id: string) {
const session = await verifySession()
const estimate = await db.query.estimates.findFirst({ where: eq(estimates.id, id) })
const canSeeCost = ['admin', 'manager'].includes(session.role)
return {
id: estimate.id,
customerName: estimate.customerName,
totalAmount: estimate.totalAmount,
cost: canSeeCost ? estimate.cost : null, // 権限がなければ値ごと落とす
}
}
画面側で {isManager && <Cost />} と書くだけだと、原価データ自体はブラウザに届いています。開発者ツールを開けば見えますし、React Server Components でも Client Component に props で渡した時点で通信に乗ります。「表示しない」と「渡さない」は別物です。ここは公式ガイドも DTO パターンとして明示的に推奨しています。
御社のシステムに「役職によって見せない項目」があるなら、画面のコードではなくデータを返す関数を確認してみるのが、いちばん確実なチェック方法です。
権限機能を後付けすると、なぜ高くつくのか
権限は横断的な関心事なので、後付けすると全機能に手を入れることになります。先に入れれば設計コストだけですが、後から入れると全画面・全APIの改修コストになります。
受託の見積もりの観点で、正直な費用感を書きます。以下はゼットリンカーで中小企業向けの社内システムを受託する際の、権限まわりに限った工数の目安です(システム全体の規模・機能数によって変動するため、あくまで目安としてご覧ください)。
| 進め方 | 権限まわりの工数目安 | 内訳 |
|---|
| 初期設計に含める | 20〜40時間 | 役割定義のヒアリング、DAL/authz の実装、テスト |
| 主要機能の実装後に追加 | 60〜120時間 | 上記+既存の全 Server Action / Route Handler への差し込み、影響範囲のリグレッションテスト |
| 本番稼働後に追加 | 120時間〜 | 上記+既存データへの役割割り当て、移行、稼働中の切り替え手順 |
差が出るのは、権限が「1機能」ではなく「全機能に薄く広がる関心事」だからです。あとから入れる場合、すべてのデータ取得関数とすべての更新処理に認可チェックを差し込み、それぞれについて「権限がない人がアクセスしたら正しく弾かれるか」をテストし直すことになります。機能数に比例して増えるので、機能が多いシステムほど後付けは高くつきます。
一方で、初期設計に含めるコストはそれほど大きくありません。上の authz.ts は実質20行程度です。高いのは実装ではなく、後から全体に差し込む作業なのです。
導入の進め方としては、次の順序を推奨しています。
- 要件定義フェーズ(1〜2週間): 役割一覧・スコープ・非表示項目の3点を業務側と確定する。ここでヒアリングするのは開発者本人が望ましいです
- 基盤実装(1週間程度): DAL・authz・DTO の3ファイルを先に作る。この時点では機能がまだ無くてよい
- 各機能の実装: 新しい Server Action / Route Handler を書くたびに、冒頭で
authorize() を呼ぶ。習慣にしてしまえば追加コストはほぼゼロ
- リリース前レビュー: 全 Server Action を grep して、
authorize() の呼び出しが漏れていないかを機械的に確認する
4番目は AI駆動開発と相性が良いところです。Claude Code のようなエージェントに「'use server' を含む全ファイルを走査して、export されている関数の冒頭で authorize() または verifySession() が呼ばれていないものを列挙して」と依頼すれば、人力の grep より確実に漏れを拾えます。私たちが Claude Code を受託で使っている実践的な使い分けは Cursor と Claude Code をどう使い分けているか にまとめています。
よくある失敗と、その回避策
権限まわりの失敗は「厳しすぎて使われない」と「緩すぎて事故る」の両極に分かれます。中小企業では、前者のほうが多く見られます。
失敗1: 役割を細かく作りすぎて、誰も維持できなくなる
記事の冒頭で予告した「逆方向の落とし穴」がこれです。役割を増やしすぎると破綻するのは直感的にわかりますが、細かく作りすぎた結果として起きるのは、権限管理の放棄という形をとります。
具体的にはこうなります。役割を12個作った。運用が始まると「この人にはこの1機能だけ使わせたい」というリクエストが来る。既存の役割に当てはまらないので13個目を作る。半年後には20個になり、誰がどの役割かを情シスが把握できなくなる。そして最終的に「面倒だから全員に管理者権限を渡す」という運用に着地します。
厳しく作りすぎた権限は、守られずに無効化されるのです。回避策はシンプルで、開始時点の役割は3〜5個に抑え、例外は所有者判定(自分の分だけ編集可)で吸収すること。役割を増やす前に、「これは新しい役割が必要なのか、それとも既存の役割にこの操作を1つ足せば済むのか」を必ず問い直します。
失敗2: 画面のボタンを隠しただけで「権限をつけた」と思っている
{isAdmin && <DeleteButton />} だけで済ませているケースです。ボタンは消えていますが、その削除処理を呼ぶ Server Action は誰でも叩けます。Server Action は公開APIと同じ扱いをすべきだ、というのが公式の立場です。
回避策は、UI の出し分けとは別に、必ずサーバー側の関数の冒頭で認可することです。UI 側はあくまで「操作できないボタンを見せない」という体験のための実装で、守りの実装ではありません。
失敗3: Proxy のマッチャーを直したら、保護が外れていた
前述の Server Function の挙動が原因の事故です。「静的ファイルが Proxy を通って遅いから」とマッチャーの除外パターンを広げた結果、そのパス配下の Server Function が認可を通らなくなる。しかもエラーにならず静かに通ってしまうので、テストで気づきにくいのが厄介です。
回避策は、そもそも Proxy に認可を依存させないこと。Proxy は「未ログインなら /login にリダイレクト」というリダイレクト集約に留め、実際の可否判断は DAL 側で持つ。この構成なら、マッチャーをどう変えてもデータは守られます。
失敗4: 退職者・異動者のアカウントが残り続ける
これは設計ではなく運用の失敗ですが、実害としてはいちばん多いパターンです。権限をどれだけ精密に設計しても、退職者のアカウントが有効なままなら意味がありません。
回避策として、私たちは最初から 「役割の有効期限」ではなく「アカウントの棚卸し画面」を用意する ことを提案しています。管理者が月1回、全アカウントの一覧を最終ログイン日つきで見られる画面が1枚あるだけで、放置は激減します。凝った承認ワークフローより、一覧が1枚ある方が実際に運用されます。
まとめ
この記事で持ち帰れることは、次の3つです。
- 社内システムの権限要件を、業務側と何を詰めるべきか判断できる(役割一覧+スコープ+非表示項目の3点セット)
- 自社の既存システムで、権限が正しい場所にあるかを確認する着眼点が分かる(画面で隠しているのか、サーバーで返していないのか)
- 権限機能を後付けする場合のコスト感を、発注前に見積もれる(初期設計なら20〜40時間、後付けなら3倍前後)
Next.js 16 で middleware.ts が proxy.ts へ改称されたことは、単なる名前の変更ではなく、「入口の1枚に守りを集中させない」という設計思想の表明です。公式が推奨する DAL パターンに沿って、データに触れる関数の入口で認可する。この形にしておけば、フレームワークの仕様変更にも、組織変更にも耐えられます。
今日ひとりでできることを1つ挙げるなら、いま社内で使っているシステムの権限一覧を、実際に紙に書き出してみることです。役割が何個あって、それぞれ誰が該当するのか。書いてみると「この役割、実は1人だけだった」「この人は本当は見えてはいけない範囲まで見えている」といった発見が、だいたい1つは出てきます。それが内製化を検討するときの、いちばん具体的な出発点になります。
ゼットリンカーでは、中小企業向けの Next.js フルスクラッチ受託で、要件定義のヒアリングをエンジニア本人が担当しています。権限設計のような「業務ルールをそのままデータ構造に落とす」領域は、営業と開発の間に伝言が入るほどズレるためです。社内システムの内製化を検討されている場合は、お問い合わせからご相談ください。役割の切り方の整理からご一緒します。
社内システムの内製そのものを検討中の方は、2026年、中小企業がNext.jsで社内アプリを内製する3つの理由 や、認証基盤の実装イメージがつかめる Next.js 16 + Firebase Authentication で実現するセキュアな会員制サイト構築 もあわせてご覧ください。セキュリティ全体の総論は 中小企業のNext.jsフルスクラッチで、セキュリティをどう担保するか に整理しています。
よくある質問
Q. 既存の middleware.ts は、すぐに proxy.ts へ移行すべきですか?
移行そのものは急ぎではありませんが、早めに済ませておくことをおすすめします。middleware は Next.js 16.0.0 で非推奨になったものの即座に動かなくなるわけではなく、公式のコードモッド(npx @next/codemod@canary middleware-to-proxy .)でファイル名と関数名を機械的に置き換えられます。ただし移行より重要なのは、middleware に認可ロジックを寄せていないかの確認です。名前を変えても設計上のリスクは残るため、認可を Data Access Layer 側へ移す作業とセットで計画してください。
Q. 役割は最初にいくつ作るのが適切ですか?
社員数十名〜200名程度の社内システムであれば、3〜5個から始めることをおすすめします。典型的には「システム管理者」「承認者(管理職)」「一般入力者」の3つで大半の要件を満たせます。役割で表現しきれない例外は、役割を増やすのではなく「自分が作成したレコードだけ編集できる」という所有者判定で吸収するほうが、運用が破綻しにくくなります。役割が10個を超えそうなら、業務ルール自体を見直すサインだと考えてよいでしょう。
Q. 権限チェックは Server Action ごとに書くのですか?共通化できませんか?
Server Action ごとに冒頭1行の authorize('操作名') を書く形を推奨しています。共通化したくなる気持ちは分かりますが、Server Action は独立したエンドポイントとして扱うべきで、フレームワーク側で自動的に全体へ適用する仕組みは用意されていません。ただし、権限の定義そのもの(どの役割がどの操作を許されるか)は1つのテーブルに集約できるので、役割が増えても修正箇所は1ファイルで済みます。呼び出し漏れは、リリース前に全 Server Action を機械的に走査して確認するのが現実的です。
Q. 権限機能を後から追加する場合、どれくらいの費用がかかりますか?
システムの機能数に比例するため一概には言えませんが、初期設計に含める場合と比べて3倍前後の工数を見込むのが安全です。権限は特定の1機能ではなく全機能に薄く広がる関心事で、後付けの場合は既存のすべてのデータ取得処理と更新処理に認可チェックを差し込み、それぞれをテストし直す必要があるためです。すでに稼働中のシステムであれば、既存ユーザーへの役割割り当てと移行手順の設計も加わります。実装そのものよりも、差し込みと検証の作業が費用の大半を占めます。
Q. Firebase Authentication を使っている場合も、同じ設計になりますか?
はい、基本的な考え方は同じです。認証(誰であるか)を Firebase Authentication が担い、認可(何をしてよいか)はアプリケーション側の Data Access Layer で持つ、という分担になります。Firebase のカスタムクレームに役割を持たせる方法もありますが、その場合も「クレームを信頼してデータを返す関数」をサーバー側に1本化する点は変わりません。認証プロバイダが何であっても、認可の判断をデータに近い場所へ寄せる、という原則は共通です。