opencode-skills-antigravity 1.0.19 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled-skills/.antigravity-install-manifest.json +1330 -0
- package/bundled-skills/akf-trust-metadata/SKILL.md +69 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/bundles.md +77 -1
- package/bundled-skills/docs/users/claude-code-skills.md +7 -1
- package/bundled-skills/docs/users/codex-cli-skills.md +7 -0
- package/bundled-skills/docs/users/faq.md +23 -0
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +10 -3
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/plugins.md +157 -0
- package/bundled-skills/docs/users/usage.md +14 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/phase-gated-debugging/SKILL.md +70 -0
- package/bundled-skills/playwright-skill/SKILL.md +8 -3
- package/bundled-skills/saas-multi-tenant/SKILL.md +183 -0
- package/bundled-skills/threejs-animation/SKILL.md +13 -8
- package/bundled-skills/threejs-fundamentals/SKILL.md +44 -1
- package/bundled-skills/threejs-geometry/SKILL.md +30 -0
- package/bundled-skills/threejs-interaction/SKILL.md +12 -0
- package/bundled-skills/threejs-lighting/SKILL.md +3 -2
- package/bundled-skills/threejs-loaders/SKILL.md +21 -1
- package/bundled-skills/threejs-materials/SKILL.md +22 -0
- package/bundled-skills/threejs-postprocessing/SKILL.md +30 -9
- package/bundled-skills/threejs-shaders/SKILL.md +43 -0
- package/bundled-skills/threejs-skills/SKILL.md +103 -42
- package/bundled-skills/threejs-textures/SKILL.md +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Plugins for Claude Code and Codex
|
|
2
|
+
|
|
3
|
+
Release `9.0.0` adds first-class plugin distributions for both **Claude Code** and **Codex**.
|
|
4
|
+
|
|
5
|
+
This page is the canonical explanation of what those plugins are, how they differ from a full library install, and why the repository now ships both a **root plugin** and multiple **bundle plugins**.
|
|
6
|
+
|
|
7
|
+
## What a plugin is in this repo
|
|
8
|
+
|
|
9
|
+
In Antigravity Awesome Skills, a plugin is a packaged, installable distribution of skills plus the metadata a host tool needs to expose that distribution through its plugin or marketplace flow.
|
|
10
|
+
|
|
11
|
+
Plugins are useful when you want:
|
|
12
|
+
|
|
13
|
+
- a marketplace-style install instead of copying files into `.claude/skills/` or `.codex/skills/`
|
|
14
|
+
- a narrower install surface for a team or role
|
|
15
|
+
- a safer default distribution for plugin ecosystems
|
|
16
|
+
|
|
17
|
+
Plugins are **not** different content formats. They still ship `SKILL.md` playbooks. The difference is the packaging, install surface, and filtering.
|
|
18
|
+
|
|
19
|
+
## Full library install vs plugin install
|
|
20
|
+
|
|
21
|
+
You now have two valid ways to use this repository with Claude Code or Codex.
|
|
22
|
+
|
|
23
|
+
### Full library install
|
|
24
|
+
|
|
25
|
+
Use the installer or clone the repository directly when you want the broadest possible coverage:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx antigravity-awesome-skills --claude
|
|
29
|
+
npx antigravity-awesome-skills --codex
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or clone manually into your preferred skills directory.
|
|
33
|
+
|
|
34
|
+
Choose the full library when you want:
|
|
35
|
+
|
|
36
|
+
- the largest available catalog
|
|
37
|
+
- repo-only skills that are still being hardened for plugin distribution
|
|
38
|
+
- direct filesystem control over the installed tree
|
|
39
|
+
|
|
40
|
+
### Plugin install
|
|
41
|
+
|
|
42
|
+
Use the plugin marketplace or repo-local plugin metadata when you want a curated, installable distribution:
|
|
43
|
+
|
|
44
|
+
- **Claude Code** uses `.claude-plugin/marketplace.json` and `.claude-plugin/plugin.json`
|
|
45
|
+
- **Codex** uses `.agents/plugins/marketplace.json` and `plugins/antigravity-awesome-skills/.codex-plugin/plugin.json`
|
|
46
|
+
|
|
47
|
+
Choose the plugin route when you want:
|
|
48
|
+
|
|
49
|
+
- marketplace-friendly installation
|
|
50
|
+
- a cleaner starter surface
|
|
51
|
+
- plugin-safe filtering by default
|
|
52
|
+
- bundle-specific installs such as `Essentials`, `Security Engineer`, or `Web Wizard`
|
|
53
|
+
|
|
54
|
+
## What `plugin-safe` means
|
|
55
|
+
|
|
56
|
+
Not every skill in the repository is immediately suitable for plugin publication.
|
|
57
|
+
|
|
58
|
+
`plugin-safe` means the published plugin excludes skills that still need hardening, portability cleanup, or explicit setup metadata. In practice, plugin-safe filtering avoids shipping skills that rely on:
|
|
59
|
+
|
|
60
|
+
- host-specific local paths
|
|
61
|
+
- undeclared manual setup
|
|
62
|
+
- assumptions that are acceptable in the full repository but too brittle for marketplace distribution
|
|
63
|
+
|
|
64
|
+
This is why the **full library** can be larger than the **plugin-safe** subset. That difference is expected and intentional.
|
|
65
|
+
|
|
66
|
+
The important rule is:
|
|
67
|
+
|
|
68
|
+
- the repository remains the source of truth for the complete library
|
|
69
|
+
- plugins publish the hardened subset that is ready for marketplace-style installation
|
|
70
|
+
|
|
71
|
+
## Root plugin vs bundle plugins
|
|
72
|
+
|
|
73
|
+
The repository now ships two plugin shapes.
|
|
74
|
+
|
|
75
|
+
### Root plugin
|
|
76
|
+
|
|
77
|
+
The root plugin is the broad installable distribution for each host:
|
|
78
|
+
|
|
79
|
+
- **Claude Code root plugin**: install the plugin-safe Antigravity library through the Claude marketplace entry
|
|
80
|
+
- **Codex root plugin**: expose the plugin-safe Antigravity library through the Codex plugin surface
|
|
81
|
+
|
|
82
|
+
Use the root plugin when you want the widest plugin-safe install without picking a specialty bundle.
|
|
83
|
+
|
|
84
|
+
### Bundle plugins
|
|
85
|
+
|
|
86
|
+
Bundle plugins are smaller, role-based distributions generated from the same repository. Examples include:
|
|
87
|
+
|
|
88
|
+
- `Essentials`
|
|
89
|
+
- `Security Engineer`
|
|
90
|
+
- `Web Wizard`
|
|
91
|
+
- `Full-Stack Developer`
|
|
92
|
+
|
|
93
|
+
Use a bundle plugin when you want:
|
|
94
|
+
|
|
95
|
+
- a lighter starting point
|
|
96
|
+
- a team-specific plugin install
|
|
97
|
+
- a curated subset instead of the broad root plugin
|
|
98
|
+
|
|
99
|
+
## Claude Code plugin surface
|
|
100
|
+
|
|
101
|
+
Claude Code uses the repository's root `.claude-plugin` metadata.
|
|
102
|
+
|
|
103
|
+
Relevant files:
|
|
104
|
+
|
|
105
|
+
- `.claude-plugin/marketplace.json`
|
|
106
|
+
- `.claude-plugin/plugin.json`
|
|
107
|
+
|
|
108
|
+
Typical install flow:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
/plugin marketplace add sickn33/antigravity-awesome-skills
|
|
112
|
+
/plugin install antigravity-awesome-skills
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Claude Code bundle plugins are also published through the same marketplace metadata, so you can install a focused bundle instead of the root plugin if you prefer.
|
|
116
|
+
|
|
117
|
+
## Codex plugin surface
|
|
118
|
+
|
|
119
|
+
Codex uses repo-local plugin metadata that points at the local plugin folders generated by this repository.
|
|
120
|
+
|
|
121
|
+
Relevant files:
|
|
122
|
+
|
|
123
|
+
- `.agents/plugins/marketplace.json`
|
|
124
|
+
- `plugins/antigravity-awesome-skills/.codex-plugin/plugin.json`
|
|
125
|
+
|
|
126
|
+
The Codex root plugin exposes the same plugin-safe library idea as Claude Code, but through Codex's plugin metadata conventions.
|
|
127
|
+
|
|
128
|
+
Bundle-specific Codex plugins are generated alongside the root plugin so you can install a narrower pack when plugin marketplaces are available in your Codex environment.
|
|
129
|
+
|
|
130
|
+
## Which path should you choose?
|
|
131
|
+
|
|
132
|
+
Choose the **full library** if:
|
|
133
|
+
|
|
134
|
+
- you want the biggest catalog
|
|
135
|
+
- you are comfortable installing directly into skills directories
|
|
136
|
+
- you want repo-only skills that are not yet published as plugins
|
|
137
|
+
|
|
138
|
+
Choose the **root plugin** if:
|
|
139
|
+
|
|
140
|
+
- you want the broad installable plugin-safe distribution
|
|
141
|
+
- you prefer marketplace-style installation
|
|
142
|
+
- you want a safer default surface for Claude Code or Codex
|
|
143
|
+
|
|
144
|
+
Choose a **bundle plugin** if:
|
|
145
|
+
|
|
146
|
+
- you want a smaller role-based install
|
|
147
|
+
- you are onboarding a team around one domain
|
|
148
|
+
- you want plugin convenience without the breadth of the root plugin
|
|
149
|
+
|
|
150
|
+
## Related guides
|
|
151
|
+
|
|
152
|
+
- [Getting Started](getting-started.md)
|
|
153
|
+
- [FAQ](faq.md)
|
|
154
|
+
- [Claude Code skills](claude-code-skills.md)
|
|
155
|
+
- [Codex CLI skills](codex-cli-skills.md)
|
|
156
|
+
- [Bundles](bundles.md)
|
|
157
|
+
- [Usage](usage.md)
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
Great question! Here's what just happened and what to do next:
|
|
10
10
|
|
|
11
|
+
If you came in through a **Claude Code** or **Codex** plugin instead of a full library install, the mental model is the same: you still invoke individual skills in prompts. The main difference is that plugins ship the plugin-safe subset. See [plugins.md](plugins.md) for the install model.
|
|
12
|
+
|
|
11
13
|
### What You Just Did
|
|
12
14
|
|
|
13
15
|
When you ran `npx antigravity-awesome-skills` or cloned the repository, you:
|
|
14
16
|
|
|
15
|
-
✅ **Downloaded 1,
|
|
17
|
+
✅ **Downloaded 1,331+ skill files** to your computer (default: `~/.gemini/antigravity/skills/`; or a custom path like `~/.agent/skills/` if you used `--path`)
|
|
16
18
|
✅ **Made them available** to your AI assistant
|
|
17
19
|
❌ **Did NOT enable them all automatically** (they're just sitting there, waiting)
|
|
18
20
|
|
|
@@ -32,7 +34,7 @@ Bundles are **curated groups** of skills organized by role. They help you decide
|
|
|
32
34
|
|
|
33
35
|
**Analogy:**
|
|
34
36
|
|
|
35
|
-
- You installed a toolbox with 1,
|
|
37
|
+
- You installed a toolbox with 1,331+ tools (✅ done)
|
|
36
38
|
- Bundles are like **labeled organizer trays** saying: "If you're a carpenter, start with these 10 tools"
|
|
37
39
|
- You can either **pick skills from the tray** or install that tray as a focused marketplace bundle plugin
|
|
38
40
|
|
|
@@ -54,6 +56,14 @@ When you see the [Web Wizard bundle](bundles.md#-the-web-wizard-pack), it lists:
|
|
|
54
56
|
|
|
55
57
|
These are **recommendations** for which skills a web developer should try first. If you have the full library installed, you just need to **use them in your prompts**. If you prefer a narrower install surface, you can install the matching bundle plugin in Claude Code or Codex where plugin marketplaces are available.
|
|
56
58
|
|
|
59
|
+
The key distinction is:
|
|
60
|
+
|
|
61
|
+
- **full library install** = broadest catalog
|
|
62
|
+
- **root plugin** = broad plugin-safe distribution
|
|
63
|
+
- **bundle plugin** = curated plugin-safe subset
|
|
64
|
+
|
|
65
|
+
See [plugins.md](plugins.md) for the canonical explanation.
|
|
66
|
+
|
|
57
67
|
If you want only one bundle active at a time in Antigravity, use the activation scripts instead of trying to invoke the bundle name directly:
|
|
58
68
|
|
|
59
69
|
```bash
|
|
@@ -202,7 +212,7 @@ Let's actually use a skill right now. Follow these steps:
|
|
|
202
212
|
|
|
203
213
|
## Step 5: Picking Your First Skills (Practical Advice)
|
|
204
214
|
|
|
205
|
-
Don't try to use all 1,
|
|
215
|
+
Don't try to use all 1,331+ skills at once. Here's a sensible approach:
|
|
206
216
|
|
|
207
217
|
If you want a tool-specific starting point before choosing skills, use:
|
|
208
218
|
|
|
@@ -333,7 +343,7 @@ Usually no, but if your AI doesn't recognize a skill:
|
|
|
333
343
|
|
|
334
344
|
### "Can I load all skills into the model at once?"
|
|
335
345
|
|
|
336
|
-
No. Even though you have 1,
|
|
346
|
+
No. Even though you have 1,331+ skills installed locally, you should **not** concatenate every `SKILL.md` into a single system prompt or context block.
|
|
337
347
|
|
|
338
348
|
The intended pattern is:
|
|
339
349
|
|
|
@@ -34,7 +34,7 @@ antigravity-awesome-skills/
|
|
|
34
34
|
├── 📄 CONTRIBUTING.md ← Contributor workflow
|
|
35
35
|
├── 📄 CATALOG.md ← Full generated catalog
|
|
36
36
|
│
|
|
37
|
-
├── 📁 skills/ ← 1,
|
|
37
|
+
├── 📁 skills/ ← 1,331+ skills live here
|
|
38
38
|
│ │
|
|
39
39
|
│ ├── 📁 brainstorming/
|
|
40
40
|
│ │ └── 📄 SKILL.md ← Skill definition
|
|
@@ -47,7 +47,7 @@ antigravity-awesome-skills/
|
|
|
47
47
|
│ │ └── 📁 2d-games/
|
|
48
48
|
│ │ └── 📄 SKILL.md ← Nested skills also supported
|
|
49
49
|
│ │
|
|
50
|
-
│ └── ... (1,
|
|
50
|
+
│ └── ... (1,331+ total)
|
|
51
51
|
│
|
|
52
52
|
├── 📁 apps/
|
|
53
53
|
│ └── 📁 web-app/ ← Interactive browser
|
|
@@ -100,7 +100,7 @@ antigravity-awesome-skills/
|
|
|
100
100
|
|
|
101
101
|
```
|
|
102
102
|
┌─────────────────────────┐
|
|
103
|
-
│ 1,
|
|
103
|
+
│ 1,331+ SKILLS │
|
|
104
104
|
└────────────┬────────────┘
|
|
105
105
|
│
|
|
106
106
|
┌────────────────────────┼────────────────────────┐
|
|
@@ -201,7 +201,7 @@ If you want a workspace-style manual install instead, cloning into `.agent/skill
|
|
|
201
201
|
│ ├── 📁 brainstorming/ │
|
|
202
202
|
│ ├── 📁 stripe-integration/ │
|
|
203
203
|
│ ├── 📁 react-best-practices/ │
|
|
204
|
-
│ └── ... (1,
|
|
204
|
+
│ └── ... (1,331+ total) │
|
|
205
205
|
└─────────────────────────────────────────┘
|
|
206
206
|
```
|
|
207
207
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: phase-gated-debugging
|
|
3
|
+
description: "Use when debugging any bug. Enforces a 5-phase protocol where code edits are blocked until root cause is confirmed. Prevents premature fix attempts."
|
|
4
|
+
risk: safe
|
|
5
|
+
source: community
|
|
6
|
+
date_added: "2026-03-28"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Phase-Gated Debugging
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
AI coding agents see an error and immediately edit code. They guess at fixes, get it wrong, and spiral. This skill enforces a strict 5-phase protocol where you CANNOT edit source code until the root cause is identified and confirmed.
|
|
14
|
+
|
|
15
|
+
Based on [claude-debug](https://github.com/krabat-l/claude-debug) (full plugin with PreToolUse hook enforcement).
|
|
16
|
+
|
|
17
|
+
## When to Use
|
|
18
|
+
|
|
19
|
+
Use this skill when:
|
|
20
|
+
|
|
21
|
+
- a bug keeps getting "fixed" without resolving the underlying issue
|
|
22
|
+
- you need to slow an agent down and force disciplined debugging before code edits
|
|
23
|
+
- the failure is intermittent, a regression, performance-related, or otherwise hard to isolate
|
|
24
|
+
- you want an explicit user confirmation checkpoint before any fix is applied
|
|
25
|
+
|
|
26
|
+
## The Protocol
|
|
27
|
+
|
|
28
|
+
### Phase 1: REPRODUCE
|
|
29
|
+
Run the failing command/test. Capture the exact error. Run 2-3 times for consistency.
|
|
30
|
+
- Do NOT read source code
|
|
31
|
+
- Do NOT hypothesize
|
|
32
|
+
- Do NOT edit any files
|
|
33
|
+
|
|
34
|
+
### Phase 2: ISOLATE
|
|
35
|
+
Read code. Add diagnostic logging marked `// DEBUG`. Re-run with diagnostics. Binary search to narrow down.
|
|
36
|
+
- Only `// DEBUG` marked logging is allowed
|
|
37
|
+
- Do NOT fix the bug even if you see it
|
|
38
|
+
|
|
39
|
+
### Phase 3: ROOT CAUSE
|
|
40
|
+
Analyze WHY at the isolated location. Use "5 Whys" technique. Remove debug logging.
|
|
41
|
+
|
|
42
|
+
State: "This is my root cause analysis: [explanation]. Do you agree, or should I investigate further?"
|
|
43
|
+
|
|
44
|
+
**WAIT for user confirmation. Do NOT proceed without it.**
|
|
45
|
+
|
|
46
|
+
### Phase 4: FIX
|
|
47
|
+
Remove all `// DEBUG` lines. Apply minimal change addressing confirmed root cause.
|
|
48
|
+
- Only edit files related to root cause
|
|
49
|
+
- Do NOT refactor unrelated code
|
|
50
|
+
|
|
51
|
+
### Phase 5: VERIFY
|
|
52
|
+
Run original failing test — must pass. Run related tests. For intermittent bugs, run 5+ times.
|
|
53
|
+
If verification fails: root cause was wrong, go back to Phase 2.
|
|
54
|
+
|
|
55
|
+
## Bug-Type Strategies
|
|
56
|
+
|
|
57
|
+
| Type | Technique |
|
|
58
|
+
|------|-----------|
|
|
59
|
+
| Crash/Panic | Stack trace backward — trace the bad value to its source |
|
|
60
|
+
| Wrong Output | Binary search — log midpoint, halve search space each iteration |
|
|
61
|
+
| Intermittent | Compare passing vs failing run logs — find ordering divergence |
|
|
62
|
+
| Regression | `git bisect` — find the offending commit |
|
|
63
|
+
| Performance | Timing at stage boundaries — find the bottleneck |
|
|
64
|
+
|
|
65
|
+
## Key Rules
|
|
66
|
+
|
|
67
|
+
1. NEVER edit source code in phases 1-3 (except `// DEBUG` in phase 2)
|
|
68
|
+
2. NEVER proceed past phase 3 without user confirmation
|
|
69
|
+
3. ALWAYS reproduce before investigating
|
|
70
|
+
4. ALWAYS verify after fixing
|
|
@@ -4,6 +4,11 @@ description: "IMPORTANT - Path Resolution: This skill can be installed in differ
|
|
|
4
4
|
risk: unknown
|
|
5
5
|
source: community
|
|
6
6
|
date_added: "2026-02-27"
|
|
7
|
+
plugin:
|
|
8
|
+
setup:
|
|
9
|
+
type: manual
|
|
10
|
+
summary: "Run `npm run setup` in the skill directory before first use to install Playwright and Chromium."
|
|
11
|
+
docs: "SKILL.md"
|
|
7
12
|
---
|
|
8
13
|
|
|
9
14
|
**IMPORTANT - Path Resolution:**
|
|
@@ -11,9 +16,9 @@ This skill can be installed in different locations (plugin system, manual instal
|
|
|
11
16
|
|
|
12
17
|
Common installation paths:
|
|
13
18
|
|
|
14
|
-
- Plugin system:
|
|
15
|
-
- Manual global:
|
|
16
|
-
- Project-specific: `<project>/.
|
|
19
|
+
- Plugin system: `<plugin-root>/skills/playwright-skill`
|
|
20
|
+
- Manual global: `<agent-home>/skills/playwright-skill`
|
|
21
|
+
- Project-specific: `<project>/.agent/skills/playwright-skill`
|
|
17
22
|
|
|
18
23
|
# Playwright Browser Automation
|
|
19
24
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: saas-multi-tenant
|
|
3
|
+
description: "Design and implement multi-tenant SaaS architectures with row-level security, tenant-scoped queries, shared-schema isolation, and safe cross-tenant admin patterns in PostgreSQL and TypeScript."
|
|
4
|
+
risk: safe
|
|
5
|
+
source: community
|
|
6
|
+
date_added: "2026-03-28"
|
|
7
|
+
tags: [multi-tenancy, saas, row-level-security, postgresql, tenant-isolation]
|
|
8
|
+
tools: [claude, cursor, gemini]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# SaaS Multi-Tenant Architecture
|
|
12
|
+
|
|
13
|
+
## When to Use This Skill
|
|
14
|
+
|
|
15
|
+
- The user is building a SaaS application where multiple customers share the same database
|
|
16
|
+
- The user asks about tenant isolation, row-level security, or data leakage prevention
|
|
17
|
+
- The user needs to scope every database query to a specific tenant without manual WHERE clauses
|
|
18
|
+
- The user asks about shared-schema vs schema-per-tenant vs database-per-tenant tradeoffs
|
|
19
|
+
- The user is implementing admin endpoints that must access data across tenants
|
|
20
|
+
- The user needs to add `tenant_id` columns to an existing single-tenant application
|
|
21
|
+
- The user asks about PostgreSQL RLS policies for tenant isolation
|
|
22
|
+
- The user is building tenant-aware middleware in Express, Fastify, or Next.js API routes
|
|
23
|
+
|
|
24
|
+
Do NOT use this skill when:
|
|
25
|
+
- The user is building a single-user application with no shared infrastructure
|
|
26
|
+
- The user asks about authentication only without tenant scoping (use an auth skill instead)
|
|
27
|
+
- The user needs general database schema design without multi-tenancy requirements
|
|
28
|
+
|
|
29
|
+
## Core Workflow
|
|
30
|
+
|
|
31
|
+
1. Determine the tenancy model. Ask the user about their scale expectations and isolation requirements. For most SaaS apps under 1000 tenants, shared-schema with a `tenant_id` column on every table is the correct default. Schema-per-tenant adds operational overhead (migrations run N times). Database-per-tenant is only justified when tenants have regulatory data residency requirements.
|
|
32
|
+
|
|
33
|
+
2. Add `tenant_id` to every tenant-scoped table. The column must be `NOT NULL`, type `UUID` or `TEXT`, and included in every composite index. Never allow a tenant-scoped table to exist without this column — a missing `tenant_id` is a data leak waiting to happen.
|
|
34
|
+
|
|
35
|
+
3. Set up PostgreSQL Row-Level Security (RLS). Create a policy on each tenant-scoped table that filters rows by `current_setting('app.current_tenant_id')`. This acts as a database-level safety net — even if application code forgets a WHERE clause, RLS blocks cross-tenant reads.
|
|
36
|
+
|
|
37
|
+
4. Build tenant-aware middleware. At the start of every request, extract the `tenant_id` from the authenticated session or JWT claims. Set it on the database connection using `SET LOCAL app.current_tenant_id = '...'` inside a transaction. Every subsequent query in that request inherits the tenant scope automatically.
|
|
38
|
+
|
|
39
|
+
5. Scope all ORM queries by tenant. If using Prisma, apply a global middleware that injects `where: { tenantId }` into every `findMany`, `findFirst`, `update`, and `delete` call. If using Drizzle, create a base query builder that includes the tenant filter. Never rely on developers remembering to add the filter manually.
|
|
40
|
+
|
|
41
|
+
6. Handle tenant-aware migrations. Every new table migration must include `tenant_id` as a column. Write a linting rule or CI check that rejects any migration creating a table without `tenant_id` unless the table is explicitly marked as global (e.g., `plans`, `feature_flags`).
|
|
42
|
+
|
|
43
|
+
7. Build cross-tenant admin routes separately. Admin endpoints that aggregate data across tenants must bypass RLS explicitly using `SET LOCAL role = 'admin_bypass'` or a dedicated database role. These routes must be protected by a separate admin authentication flow — never reuse tenant user sessions for admin access.
|
|
44
|
+
|
|
45
|
+
8. Implement tenant provisioning. When a new customer signs up, create their tenant record, seed default data (roles, settings, onboarding state), and assign the founding user. Wrap this in a database transaction so partial provisioning never leaves orphan records.
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
### Example 1: PostgreSQL RLS Policy for Tenant Isolation
|
|
50
|
+
|
|
51
|
+
```sql
|
|
52
|
+
-- Enable RLS on the table
|
|
53
|
+
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
|
54
|
+
ALTER TABLE projects FORCE ROW LEVEL SECURITY;
|
|
55
|
+
|
|
56
|
+
-- Policy: users can only see rows where tenant_id matches the session variable
|
|
57
|
+
CREATE POLICY tenant_isolation ON projects
|
|
58
|
+
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
|
|
59
|
+
|
|
60
|
+
-- Policy for INSERT: new rows must match the current tenant
|
|
61
|
+
CREATE POLICY tenant_insert ON projects
|
|
62
|
+
FOR INSERT
|
|
63
|
+
WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Example 2: Express Middleware That Sets Tenant Context per Request
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { Pool } from "pg";
|
|
70
|
+
|
|
71
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
72
|
+
|
|
73
|
+
async function tenantMiddleware(req, res, next) {
|
|
74
|
+
const tenantId = req.auth?.tenantId; // extracted from JWT during auth
|
|
75
|
+
if (!tenantId) return res.status(403).json({ error: "No tenant context" });
|
|
76
|
+
|
|
77
|
+
const client = await pool.connect();
|
|
78
|
+
try {
|
|
79
|
+
await client.query("BEGIN");
|
|
80
|
+
// Use set_config — SET LOCAL does not accept bind placeholders ($1)
|
|
81
|
+
await client.query("SELECT set_config('app.current_tenant_id', $1, true)", [tenantId]);
|
|
82
|
+
req.db = client;
|
|
83
|
+
req.tenantId = tenantId;
|
|
84
|
+
|
|
85
|
+
// Cleanup on response finish — guarantees release even if handler skips next()
|
|
86
|
+
res.on("finish", async () => {
|
|
87
|
+
try { await client.query("COMMIT"); } catch { await client.query("ROLLBACK"); }
|
|
88
|
+
client.release();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
next();
|
|
92
|
+
} catch (err) {
|
|
93
|
+
await client.query("ROLLBACK").catch(() => {});
|
|
94
|
+
client.release();
|
|
95
|
+
next(err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Example 3: Prisma Middleware for Automatic Tenant Scoping
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { PrismaClient } from "@prisma/client";
|
|
104
|
+
|
|
105
|
+
// Tables that do NOT have tenant_id (global tables)
|
|
106
|
+
const GLOBAL_TABLES = new Set(["Plan", "FeatureFlag", "SystemConfig"]);
|
|
107
|
+
|
|
108
|
+
function createTenantPrisma(tenantId: string): PrismaClient {
|
|
109
|
+
const prisma = new PrismaClient();
|
|
110
|
+
|
|
111
|
+
prisma.$use(async (params, next) => {
|
|
112
|
+
if (GLOBAL_TABLES.has(params.model ?? "")) return next(params);
|
|
113
|
+
|
|
114
|
+
// Initialize args.where — Prisma passes undefined args for calls like findMany()
|
|
115
|
+
params.args = params.args ?? {};
|
|
116
|
+
params.args.where = params.args.where ?? {};
|
|
117
|
+
|
|
118
|
+
// Inject tenant filter on reads (skip findUnique — it only accepts unique-field selectors)
|
|
119
|
+
if (["findMany", "findFirst", "count", "aggregate"].includes(params.action)) {
|
|
120
|
+
params.args.where = { ...params.args.where, tenantId };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Inject tenant_id on creates
|
|
124
|
+
if (["create", "createMany"].includes(params.action)) {
|
|
125
|
+
params.args.data = params.args.data ?? {};
|
|
126
|
+
if (params.action === "createMany") {
|
|
127
|
+
params.args.data = params.args.data.map((d: any) => ({ ...d, tenantId }));
|
|
128
|
+
} else {
|
|
129
|
+
params.args.data = { ...params.args.data, tenantId };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Scope updates and deletes
|
|
134
|
+
if (["update", "updateMany", "delete", "deleteMany"].includes(params.action)) {
|
|
135
|
+
params.args.where = { ...params.args.where, tenantId };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return next(params);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return prisma;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Never Do This
|
|
146
|
+
|
|
147
|
+
1. **Never query a tenant-scoped table without a `tenant_id` filter.** Even if your ORM middleware handles it, raw SQL queries bypass middleware entirely. Every raw query must include `WHERE tenant_id = $1` or rely on RLS. A single unscoped `SELECT * FROM invoices` leaks every customer's billing data.
|
|
148
|
+
|
|
149
|
+
2. **Never store `tenant_id` only in the application session without enforcing it at the database level.** Application-layer filtering is a suggestion. RLS is enforcement. If a bug in your middleware skips the tenant filter, only RLS prevents the data leak. Run both layers.
|
|
150
|
+
|
|
151
|
+
3. **Never use auto-incrementing integer IDs for tenant-scoped resources.** Sequential IDs (`invoice #1042`) let attackers enumerate other tenants' resources by incrementing the ID. Use UUIDs for all tenant-scoped primary keys. Reserve integer IDs for internal-only tables.
|
|
152
|
+
|
|
153
|
+
4. **Never let tenant users access admin aggregation endpoints.** A route like `GET /admin/metrics` that queries across all tenants must never be reachable with a regular tenant JWT. Use a separate authentication mechanism (API key, admin role claim with a different issuer) for cross-tenant routes.
|
|
154
|
+
|
|
155
|
+
5. **Never run migrations with RLS enabled on the migration connection.** The migration user needs to create tables, add columns, and modify policies. If RLS is active on the migration connection, `ALTER TABLE` commands may silently fail or affect only the "current tenant's" view. Use a dedicated superuser or `bypassrls` role for migrations.
|
|
156
|
+
|
|
157
|
+
6. **Never share connection pools across tenants when using `SET LOCAL`.** If you use `SET LOCAL app.current_tenant_id` inside a transaction, that setting is scoped to the transaction. But if a previous request's transaction was not properly committed or rolled back, the connection returns to the pool with stale tenant context. Always `RESET app.current_tenant_id` in the cleanup path.
|
|
158
|
+
|
|
159
|
+
## Edge Cases
|
|
160
|
+
|
|
161
|
+
1. **Tenant deletion and data retention.** When a tenant cancels their subscription, you cannot simply `DELETE FROM tenants WHERE id = $1`. Foreign key cascades may time out on large datasets. Instead, soft-delete the tenant (set `deleted_at`), revoke all user sessions, then run a background job that deletes tenant data in batches over hours or days.
|
|
162
|
+
|
|
163
|
+
2. **Tenant data export for GDPR/compliance.** When a tenant requests a full data export, you need to query every tenant-scoped table for that `tenant_id` and package it. Build a registry of all tenant-scoped tables (parse your migration files or maintain a manifest) so the export job doesn't miss tables added after the export feature was built.
|
|
164
|
+
|
|
165
|
+
3. **Shared resources between tenants.** Some features require shared state — e.g., a marketplace where Tenant A's products are visible to Tenant B's users. These tables need a different RLS policy: read access is public (no tenant filter), but write access is still scoped to the owning tenant. Model these as `owner_tenant_id` instead of `tenant_id`.
|
|
166
|
+
|
|
167
|
+
4. **Tenant-aware background jobs.** When a cron job or queue worker processes tasks, there is no HTTP request to extract `tenant_id` from. The job payload must include `tenant_id`, and the worker must set the database session variable before processing. Never run background jobs without tenant context — they will either fail on RLS or bypass it entirely.
|
|
168
|
+
|
|
169
|
+
5. **Connection pool exhaustion with schema-per-tenant.** If you use one PostgreSQL schema per tenant and each schema requires its own connection pool, 500 tenants means 500 pools. This exhausts `max_connections` fast. Use a connection pooler like PgBouncer in transaction mode, or switch to shared-schema before hitting this wall.
|
|
170
|
+
|
|
171
|
+
## Best Practices
|
|
172
|
+
|
|
173
|
+
1. **Create a `tenants` table as the single source of truth.** Every `tenant_id` foreign key in every table points back to `tenants.id`. Include columns for `name`, `slug` (for subdomain routing), `plan_id`, `created_at`, and `deleted_at`. This table is the root of your entire data model.
|
|
174
|
+
|
|
175
|
+
2. **Index `tenant_id` as the first column in every composite index.** PostgreSQL uses leftmost prefix matching for composite indexes. An index on `(tenant_id, created_at)` serves both "all items for tenant X" and "items for tenant X sorted by date." An index on `(created_at, tenant_id)` only helps date-range queries across all tenants.
|
|
176
|
+
|
|
177
|
+
3. **Use subdomains or path prefixes for tenant routing.** `acme.yourapp.com` or `yourapp.com/org/acme` — both work. Map the subdomain or path to a `tenant_id` lookup at the edge (middleware or reverse proxy). This lookup should be cached (Redis or in-memory with 60s TTL) since it runs on every single request.
|
|
178
|
+
|
|
179
|
+
4. **Separate tenant-scoped tables from global tables explicitly.** Maintain a list (code constant or database table) of which tables are global (no `tenant_id`) and which are tenant-scoped. Use this list in your ORM middleware, your migration linter, and your data export job. If a table isn't in either list, the CI check should fail.
|
|
180
|
+
|
|
181
|
+
5. **Test with at least 3 tenants in your seed data.** A single tenant in development hides every multi-tenancy bug. Two tenants hides bugs where the first tenant's data leaks to the second but not vice versa. Three tenants catches ordering and filtering bugs that only appear with multiple peers.
|
|
182
|
+
|
|
183
|
+
6. **Rate-limit and quota per tenant, not globally.** A global rate limit of 1000 requests/minute means one noisy tenant can exhaust the quota for everyone. Implement per-tenant rate limiting using a Redis key pattern like `ratelimit:{tenant_id}:{endpoint}` with a sliding window counter.
|
|
@@ -12,22 +12,23 @@ source: community
|
|
|
12
12
|
```javascript
|
|
13
13
|
import * as THREE from "three";
|
|
14
14
|
|
|
15
|
-
// Simple procedural animation
|
|
16
|
-
const
|
|
15
|
+
// Simple procedural animation with Timer (recommended in r183)
|
|
16
|
+
const timer = new THREE.Timer();
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
18
|
+
renderer.setAnimationLoop(() => {
|
|
19
|
+
timer.update();
|
|
20
|
+
const delta = timer.getDelta();
|
|
21
|
+
const elapsed = timer.getElapsed();
|
|
21
22
|
|
|
22
23
|
mesh.rotation.y += delta;
|
|
23
24
|
mesh.position.y = Math.sin(elapsed) * 0.5;
|
|
24
25
|
|
|
25
|
-
requestAnimationFrame(animate);
|
|
26
26
|
renderer.render(scene, camera);
|
|
27
|
-
}
|
|
28
|
-
animate();
|
|
27
|
+
});
|
|
29
28
|
```
|
|
30
29
|
|
|
30
|
+
**Note:** `THREE.Timer` is recommended over `THREE.Clock` as of r183. Timer pauses when the page is hidden and has a cleaner API. `THREE.Clock` still works but is considered legacy.
|
|
31
|
+
|
|
31
32
|
## Animation System Overview
|
|
32
33
|
|
|
33
34
|
Three.js animation system has three main components:
|
|
@@ -118,6 +119,10 @@ track.setInterpolation(THREE.InterpolateSmooth); // Cubic spline
|
|
|
118
119
|
track.setInterpolation(THREE.InterpolateDiscrete); // Step function
|
|
119
120
|
```
|
|
120
121
|
|
|
122
|
+
### BezierInterpolant (r183)
|
|
123
|
+
|
|
124
|
+
Three.js r183 adds `THREE.BezierInterpolant` for bezier curve interpolation in keyframe tracks, enabling smoother animation curves with tangent control.
|
|
125
|
+
|
|
121
126
|
## AnimationMixer
|
|
122
127
|
|
|
123
128
|
Plays animations on an object and its descendants.
|
|
@@ -416,7 +416,24 @@ function dispose() {
|
|
|
416
416
|
}
|
|
417
417
|
```
|
|
418
418
|
|
|
419
|
-
### Clock for Animation
|
|
419
|
+
### Timer and Clock for Animation
|
|
420
|
+
|
|
421
|
+
**Timer (recommended in r183)** - pauses when tab is hidden, cleaner API:
|
|
422
|
+
|
|
423
|
+
```javascript
|
|
424
|
+
const timer = new THREE.Timer();
|
|
425
|
+
|
|
426
|
+
renderer.setAnimationLoop(() => {
|
|
427
|
+
timer.update();
|
|
428
|
+
const delta = timer.getDelta();
|
|
429
|
+
const elapsed = timer.getElapsed();
|
|
430
|
+
|
|
431
|
+
mesh.rotation.y += delta * 0.5;
|
|
432
|
+
renderer.render(scene, camera);
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**Clock (legacy, still works):**
|
|
420
437
|
|
|
421
438
|
```javascript
|
|
422
439
|
const clock = new THREE.Clock();
|
|
@@ -432,6 +449,17 @@ function animate() {
|
|
|
432
449
|
}
|
|
433
450
|
```
|
|
434
451
|
|
|
452
|
+
### Animation Loop
|
|
453
|
+
|
|
454
|
+
Prefer `renderer.setAnimationLoop()` over manual `requestAnimationFrame`. It handles WebXR compatibility and is the standard Three.js pattern:
|
|
455
|
+
|
|
456
|
+
```javascript
|
|
457
|
+
renderer.setAnimationLoop(() => {
|
|
458
|
+
controls.update();
|
|
459
|
+
renderer.render(scene, camera);
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
435
463
|
### Responsive Canvas
|
|
436
464
|
|
|
437
465
|
```javascript
|
|
@@ -483,6 +511,21 @@ lod.addLevel(lowDetailMesh, 100);
|
|
|
483
511
|
scene.add(lod);
|
|
484
512
|
```
|
|
485
513
|
|
|
514
|
+
## WebGPU Renderer (r183)
|
|
515
|
+
|
|
516
|
+
Three.js includes an experimental WebGPU renderer as an alternative to WebGL:
|
|
517
|
+
|
|
518
|
+
```javascript
|
|
519
|
+
import { WebGPURenderer } from "three/addons/renderers/webgpu/WebGPURenderer.js";
|
|
520
|
+
|
|
521
|
+
const renderer = new WebGPURenderer({ antialias: true });
|
|
522
|
+
await renderer.init();
|
|
523
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
524
|
+
document.body.appendChild(renderer.domElement);
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
WebGPU uses TSL (Three.js Shading Language) instead of GLSL. The WebGL renderer remains the default and is fully supported.
|
|
528
|
+
|
|
486
529
|
## See Also
|
|
487
530
|
|
|
488
531
|
- `threejs-geometry` - Geometry creation and manipulation
|
|
@@ -543,6 +543,36 @@ new THREE.SphereGeometry(1, 16, 16); // Performance mode
|
|
|
543
543
|
geometry.dispose();
|
|
544
544
|
```
|
|
545
545
|
|
|
546
|
+
## BatchedMesh (r183)
|
|
547
|
+
|
|
548
|
+
`BatchedMesh` is a higher-level alternative to `InstancedMesh` that supports multiple geometries in a single draw call. As of r183, it supports **per-instance opacity** and **per-instance wireframe**.
|
|
549
|
+
|
|
550
|
+
```javascript
|
|
551
|
+
const batchedMesh = new THREE.BatchedMesh(maxGeometryCount, maxVertexCount, maxIndexCount);
|
|
552
|
+
batchedMesh.sortObjects = true; // Enable depth sorting for transparency
|
|
553
|
+
|
|
554
|
+
// Add different geometries
|
|
555
|
+
const boxId = batchedMesh.addGeometry(new THREE.BoxGeometry(1, 1, 1));
|
|
556
|
+
const sphereId = batchedMesh.addGeometry(new THREE.SphereGeometry(0.5, 16, 16));
|
|
557
|
+
|
|
558
|
+
// Add instances of those geometries
|
|
559
|
+
const instance1 = batchedMesh.addInstance(boxId);
|
|
560
|
+
const instance2 = batchedMesh.addInstance(sphereId);
|
|
561
|
+
|
|
562
|
+
// Set transforms
|
|
563
|
+
const matrix = new THREE.Matrix4();
|
|
564
|
+
matrix.setPosition(2, 0, 0);
|
|
565
|
+
batchedMesh.setMatrixAt(instance1, matrix);
|
|
566
|
+
|
|
567
|
+
// Per-instance opacity (r183)
|
|
568
|
+
batchedMesh.setOpacityAt(instance1, 0.5);
|
|
569
|
+
|
|
570
|
+
// Per-instance visibility
|
|
571
|
+
batchedMesh.setVisibleAt(instance2, false);
|
|
572
|
+
|
|
573
|
+
scene.add(batchedMesh);
|
|
574
|
+
```
|
|
575
|
+
|
|
546
576
|
## See Also
|
|
547
577
|
|
|
548
578
|
- `threejs-fundamentals` - Scene setup and Object3D
|