@jaypie/mcp 0.7.37 → 0.7.39

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.
@@ -9,7 +9,7 @@ import { gt } from 'semver';
9
9
  /**
10
10
  * Docs Suite - Documentation services (skill, version, release_notes)
11
11
  */
12
- const BUILD_VERSION_STRING = "@jaypie/mcp@0.7.37#b65e6395"
12
+ const BUILD_VERSION_STRING = "@jaypie/mcp@0.7.39#e7380f8a"
13
13
  ;
14
14
  const __filename$1 = fileURLToPath(import.meta.url);
15
15
  const __dirname$1 = path.dirname(__filename$1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaypie/mcp",
3
- "version": "0.7.37",
3
+ "version": "0.7.39",
4
4
  "description": "Jaypie MCP",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,17 @@
1
+ ---
2
+ version: 1.2.16
3
+ date: 2026-03-14
4
+ summary: Fix setup array mutation causing duplicate execution on warm Lambda containers
5
+ ---
6
+
7
+ # @jaypie/express 1.2.16
8
+
9
+ ## Bug Fixes
10
+
11
+ - **Setup array mutation on warm containers**: `expressHandler` and `expressStreamHandler` mutated the shared `setup` array via `unshift`/`push` on every request. On warm Lambda containers, this caused `loadEnvSecrets` and other setup functions to accumulate and execute multiple times per invoke. Now builds a request-local setup array each invocation, leaving the shared array untouched.
12
+
13
+ ## Impact
14
+
15
+ - Eliminates duplicate `loadEnvSecrets` execution per request
16
+ - Eliminates increasing duplication over time on warm containers
17
+ - Reduces unnecessary Secrets Manager traffic and latency
@@ -0,0 +1,12 @@
1
+ ---
2
+ version: 1.2.22
3
+ date: 2026-03-14
4
+ summary: Bump @jaypie/express and @jaypie/kit for warm container and logging fixes
5
+ ---
6
+
7
+ # jaypie 1.2.22
8
+
9
+ ## Dependencies
10
+
11
+ - `@jaypie/express` 1.2.15 -> 1.2.16: Fix setup array mutation causing duplicate execution on warm Lambda containers
12
+ - `@jaypie/kit` 1.2.4 -> 1.2.5: Use shared logger singleton so invoke tags propagate to kit lifecycle logs
@@ -0,0 +1,16 @@
1
+ ---
2
+ version: 1.2.5
3
+ date: 2026-03-14
4
+ summary: Use shared logger singleton so invoke/shortInvoke tags propagate to kit lifecycle logs
5
+ ---
6
+
7
+ # @jaypie/kit 1.2.5
8
+
9
+ ## Bug Fixes
10
+
11
+ - **Logger invoke tags not propagating**: `@jaypie/kit` created its own logger instance via `createLogger()` instead of using the shared `@jaypie/logger` singleton. Tags like `invoke` and `shortInvoke` set by upstream adapters (e.g., `@jaypie/express`) never reached kit lifecycle logs (`[jaypie] Handler init`, `[handler] Setup`, etc.). Now uses the shared singleton so all tags propagate across packages.
12
+
13
+ ## Impact
14
+
15
+ - Datadog queries on `@invoke:<uuid>` now include `@jaypie/kit` lifecycle logs
16
+ - Full correlation across middleware/framework layers in a single invoke
@@ -0,0 +1,19 @@
1
+ ---
2
+ version: 0.7.38
3
+ date: 2026-03-14
4
+ summary: Unify skill documentation with cross-linking, recipe guides, and @jaypie/dynamodb runtime docs
5
+ ---
6
+
7
+ ## Changes
8
+
9
+ - Expanded `dynamodb` skill with full `@jaypie/dynamodb` runtime package documentation (entity operations, GSI queries, scope hierarchy, seed/export, key builders)
10
+ - Added GSI guidance: start with zero, add one per deploy, CLI recommended for production
11
+ - Clarified key naming conventions: `model`/`id` (Jaypie default) vs `pk`/`sk` (generic DynamoDB)
12
+ - Added `Lambda with DynamoDB Tables` and `Lambda with Secrets` sections to `cdk` skill
13
+ - Documented personal environment secret consumer gotcha in `secrets` skill with `consumer: false` fix
14
+ - Added version note for `seed` support and DynamoDB hash storage pattern to `apikey` skill
15
+ - Added missing environment variables to `variables` skill (PROJECT_SALT, PROJECT_ADMIN_SEED, CDK_ENV_PERSONAL, DYNAMODB_TABLE_NAME, BASE_URL, PROJECT_BASE_URL)
16
+ - Created `recipe-api-server` skill: end-to-end guide for API server with DynamoDB + API keys
17
+ - Added `## See Also` sections to 11 skills for consistent cross-linking
18
+ - Added category taxonomy to `index` skill
19
+ - Added `recipes` category to skill index
@@ -0,0 +1,12 @@
1
+ ---
2
+ version: 0.7.39
3
+ date: 2026-03-14
4
+ summary: Add release notes for @jaypie/express 1.2.16 and jaypie 1.2.22
5
+ ---
6
+
7
+ # @jaypie/mcp 0.7.39
8
+
9
+ ## Documentation
10
+
11
+ - Added release notes for `@jaypie/express` 1.2.16 (setup array mutation fix)
12
+ - Added release notes for `jaypie` 1.2.22 (dependency bump)
package/skills/apikey.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: API key generation, validation, and hashing with Jaypie keys
3
- related: secrets, style, tests
3
+ related: dynamodb, secrets, style, tests
4
4
  ---
5
5
 
6
6
  # API Keys
@@ -43,6 +43,8 @@ generateJaypieKey({ prefix: "", issuer: "jaypie" });
43
43
 
44
44
  ### With Seed
45
45
 
46
+ > **Version note:** `seed` support requires `jaypie >= 1.2.17` / `@jaypie/kit >= 1.2.2`.
47
+
46
48
  Pass `seed` to derive a deterministic key from a secret. Uses HMAC-SHA256 with the `issuer` (defaulting to `"jaypie"`) as the HMAC message:
47
49
 
48
50
  ```typescript
@@ -211,3 +213,21 @@ new JaypieEnvSecret(this, "ProjectAdminSeed", {
211
213
  ```
212
214
 
213
215
  See `~secrets` for the full secrets management pattern.
216
+
217
+ ## DynamoDB Storage Pattern
218
+
219
+ Store hashed API keys in DynamoDB for direct lookup without a GSI:
220
+
221
+ ```typescript
222
+ // model = "apikey", id = hash — enables direct get-item lookup
223
+ { model: "apikey", id: hashJaypieKey(key), ownerId: "user_123", createdAt: "..." }
224
+ ```
225
+
226
+ This uses the `JaypieDynamoDb` default key convention (`model`/`id`). See `skill("dynamodb")` for table setup and query patterns.
227
+
228
+ ## See Also
229
+
230
+ - **`skill("cdk")`** - CDK constructs for Lambda with secrets and tables
231
+ - **`skill("dynamodb")`** - DynamoDB key conventions and query patterns
232
+ - **`skill("secrets")`** - Generated secrets for PROJECT_SALT and PROJECT_ADMIN_SEED
233
+ - **`skill("recipe-api-server")`** - End-to-end guide for API server with DynamoDB + API keys
package/skills/aws.md CHANGED
@@ -277,3 +277,11 @@ describe("Handler", () => {
277
277
  });
278
278
  });
279
279
  ```
280
+
281
+ ## See Also
282
+
283
+ - **`skill("cdk")`** - CDK constructs for deploying AWS infrastructure
284
+ - **`skill("dynamodb")`** - DynamoDB key design and query patterns
285
+ - **`skill("lambda")`** - Lambda handler wrappers with secrets loading
286
+ - **`skill("secrets")`** - Secret management with JaypieEnvSecret
287
+ - **`skill("tools-aws")`** - Interactive AWS MCP tools
package/skills/cdk.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: CDK constructs and deployment patterns
3
- related: aws, cicd, dynamodb, streaming, websockets
3
+ related: apikey, aws, cicd, dynamodb, express, lambda, secrets, streaming, websockets
4
4
  ---
5
5
 
6
6
  # CDK Constructs
@@ -120,6 +120,79 @@ PROJECT_ENV=sandbox PROJECT_NONCE=dev cdk deploy
120
120
  PROJECT_ENV=production PROJECT_NONCE=prod cdk deploy
121
121
  ```
122
122
 
123
+ ## Lambda with DynamoDB Tables
124
+
125
+ Pass tables to `JaypieLambda` for automatic permissions and environment wiring:
126
+
127
+ ```typescript
128
+ import { JaypieDynamoDb, JaypieLambda } from "@jaypie/constructs";
129
+
130
+ const table = new JaypieDynamoDb(this, "myApp");
131
+
132
+ const handler = new JaypieLambda(this, "ApiHandler", {
133
+ entry: "src/handler.ts",
134
+ handler: "handler",
135
+ tables: [table],
136
+ });
137
+ ```
138
+
139
+ **Behavior:**
140
+
141
+ | Tables Count | Permissions | Environment |
142
+ |-------------|-------------|-------------|
143
+ | 1 table | `grantReadWriteData()` | `DYNAMODB_TABLE_NAME` set automatically |
144
+ | 2+ tables | `grantReadWriteData()` for each | No auto env var — set `CDK_ENV_TABLE` manually |
145
+
146
+ Single table example — at runtime, use `process.env.DYNAMODB_TABLE_NAME`:
147
+
148
+ ```typescript
149
+ const table = new JaypieDynamoDb(this, "myApp");
150
+ new JaypieLambda(this, "Handler", {
151
+ tables: [table],
152
+ // DYNAMODB_TABLE_NAME is set automatically
153
+ });
154
+ ```
155
+
156
+ Multi-table example — set environment variables explicitly:
157
+
158
+ ```typescript
159
+ const usersTable = new JaypieDynamoDb(this, "users");
160
+ const ordersTable = new JaypieDynamoDb(this, "orders");
161
+
162
+ new JaypieLambda(this, "Handler", {
163
+ tables: [usersTable, ordersTable],
164
+ environment: {
165
+ USERS_TABLE: usersTable.tableName,
166
+ ORDERS_TABLE: ordersTable.tableName,
167
+ },
168
+ });
169
+ ```
170
+
171
+ See `skill("dynamodb")` for table key conventions and query patterns.
172
+
173
+ ## Lambda with Secrets
174
+
175
+ Pass secrets as strings (auto-creates `JaypieEnvSecret`) or as construct instances:
176
+
177
+ ```typescript
178
+ // String shorthand — creates JaypieEnvSecret from env vars at deploy time
179
+ new JaypieLambda(this, "Handler", {
180
+ secrets: ["MONGODB_URI", "ANTHROPIC_API_KEY"],
181
+ });
182
+
183
+ // Construct instances — for generated secrets or custom config
184
+ const salt = new JaypieEnvSecret(this, "ProjectSalt", {
185
+ envKey: "PROJECT_SALT",
186
+ generateSecretString: { excludePunctuation: true, passwordLength: 64 },
187
+ });
188
+
189
+ new JaypieLambda(this, "Handler", {
190
+ secrets: [salt, "ANTHROPIC_API_KEY"],
191
+ });
192
+ ```
193
+
194
+ See `skill("secrets")` for the full secrets pattern including generated secrets and personal environment gotchas.
195
+
123
196
  ## Environment Variables
124
197
 
125
198
  Pass configuration to Lambda via environment variables:
@@ -207,5 +280,11 @@ new JaypieDistribution(this, "Dist", {
207
280
 
208
281
  ## See Also
209
282
 
283
+ - **`skill("apikey")`** - API key generation, validation, and hashing
284
+ - **`skill("dynamodb")`** - DynamoDB key design and query patterns
285
+ - **`skill("express")`** - Express handler and Lambda adapter
286
+ - **`skill("lambda")`** - Lambda handler wrappers and lifecycle
287
+ - **`skill("secrets")`** - Secret management with JaypieEnvSecret
210
288
  - **`skill("streaming")`** - JaypieDistribution and JaypieNextJs streaming configuration
289
+ - **`skill("variables")`** - Environment variables reference
211
290
  - **`skill("websockets")`** - JaypieWebSocket and JaypieWebSocketTable constructs
package/skills/datadog.md CHANGED
@@ -192,3 +192,10 @@ describe("CheckoutService", () => {
192
192
  });
193
193
  ```
194
194
 
195
+ ## See Also
196
+
197
+ - **`skill("cdk")`** - CDK Lambda construct with Datadog integration
198
+ - **`skill("logs")`** - Logging patterns for structured observability
199
+ - **`skill("tools-datadog")`** - Interactive Datadog MCP tools
200
+ - **`skill("variables")`** - Datadog and environment variables reference
201
+
package/skills/dns.md CHANGED
@@ -132,3 +132,7 @@ dig @ns-123.awsdns-45.com app.example.com
132
132
  openssl s_client -connect app.example.com:443 -servername app.example.com
133
133
  ```
134
134
 
135
+ ## See Also
136
+
137
+ - **`skill("cdk")`** - CDK constructs including JaypieDistribution and JaypieNextJs
138
+
@@ -1,108 +1,342 @@
1
1
  ---
2
- description: DynamoDB code patterns, key design, and queries
3
- related: aws, cdk, models, tools-dynamodb
2
+ description: DynamoDB runtime package, key design, entity operations, and queries
3
+ related: apikey, aws, cdk, models, tools-dynamodb
4
4
  ---
5
5
 
6
6
  # DynamoDB Patterns
7
7
 
8
- Best practices for DynamoDB with Jaypie applications.
8
+ Jaypie provides `@jaypie/dynamodb` for single-table DynamoDB with entity operations, GSI-based queries, hierarchical scoping, and soft delete. Access through the main `jaypie` package or directly.
9
9
 
10
10
  ## MCP Tools
11
11
 
12
12
  For interactive DynamoDB tools (query, scan, get-item, describe-table), see **tools-dynamodb**.
13
13
 
14
- ## Key Design
14
+ ## Key Naming Conventions
15
15
 
16
- ### Single Table Design
16
+ Jaypie uses two key naming patterns. Understanding when to use each avoids confusion.
17
17
 
18
- Use prefixed keys for multiple entity types:
18
+ ### Jaypie Default: `model` / `id`
19
+
20
+ `JaypieDynamoDb` creates tables with `model` (partition key) and `id` (sort key) by default. This is the **recommended convention for new Jaypie tables** and the convention used by `@jaypie/dynamodb`:
19
21
 
20
22
  ```typescript
21
- // User entity
22
- {
23
- pk: "USER#123",
24
- sk: "PROFILE",
25
- name: "John Doe",
26
- email: "john@example.com"
27
- }
23
+ const table = new JaypieDynamoDb(this, "myApp");
24
+ // Creates table with: model (HASH), id (RANGE)
25
+ ```
28
26
 
29
- // User's orders
30
- {
31
- pk: "USER#123",
32
- sk: "ORDER#2024-01-15#abc",
33
- total: 99.99,
34
- status: "completed"
35
- }
27
+ Items look like:
28
+
29
+ ```typescript
30
+ { model: "user", id: "u_abc123", name: "John", email: "john@example.com" }
31
+ { model: "apikey", id: "a1b2c3d4...", ownerId: "u_abc123" }
36
32
  ```
37
33
 
38
- ### Access Patterns
34
+ ### Generic DynamoDB: `pk` / `sk`
35
+
36
+ The `pk` / `sk` pattern is the standard DynamoDB convention used in broader DynamoDB literature. Jaypie uses it in local development scripts and educational examples. It's functionally equivalent — just different attribute names.
37
+
38
+ ### Entity Prefixing (Value Pattern)
39
+
40
+ Entity prefixes like `USER#123` are a **value-level pattern** (how you structure the data stored in keys), not an attribute naming convention. You can use entity prefixes with either `model`/`id` or `pk`/`sk` attribute names.
39
41
 
40
- | Access Pattern | Key Condition |
41
- |---------------|---------------|
42
- | Get user profile | pk = USER#123, sk = PROFILE |
43
- | List user orders | pk = USER#123, sk begins_with ORDER# |
44
- | Get specific order | pk = USER#123, sk = ORDER#2024-01-15#abc |
42
+ ## @jaypie/dynamodb Package
45
43
 
46
- ## Query in Code
44
+ The runtime package provides entity operations, GSI-based queries, key builders, and client management.
47
45
 
48
46
  ```typescript
49
- import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
50
- import { QueryCommand } from "@aws-sdk/lib-dynamodb";
47
+ import {
48
+ APEX,
49
+ initClient,
50
+ putEntity,
51
+ getEntity,
52
+ deleteEntity,
53
+ queryByScope,
54
+ queryByCategory,
55
+ } from "@jaypie/dynamodb";
56
+ ```
51
57
 
52
- const client = new DynamoDBClient({});
58
+ Or through the main package:
53
59
 
54
- const result = await client.send(new QueryCommand({
55
- TableName: process.env.CDK_ENV_TABLE,
56
- KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)",
57
- ExpressionAttributeValues: {
58
- ":pk": "USER#123",
59
- ":prefix": "ORDER#",
60
- },
61
- ScanIndexForward: false, // Newest first
62
- Limit: 10,
63
- }));
60
+ ```typescript
61
+ import { APEX, initClient, putEntity, queryByScope } from "jaypie";
64
62
  ```
65
63
 
66
- ## GSI Patterns
64
+ ### Client Initialization
67
65
 
68
- Start with zero GSIs. Add indexes only when a real access pattern requires them — GSIs cost money and can always be added later.
66
+ Call `initClient()` once at startup. In Lambda, use the handler's `setup` option:
67
+
68
+ ```typescript
69
+ import { initClient } from "@jaypie/dynamodb";
69
70
 
70
- ### By-Status Index (example)
71
+ // Production: uses DYNAMODB_TABLE_NAME and AWS_REGION env vars
72
+ initClient();
73
+
74
+ // Local development: explicit config
75
+ initClient({
76
+ endpoint: "http://127.0.0.1:8000",
77
+ tableName: "jaypie-local",
78
+ credentials: { accessKeyId: "local", secretAccessKey: "local" },
79
+ });
80
+ ```
81
+
82
+ | Function | Description |
83
+ |----------|-------------|
84
+ | `initClient(config?)` | Initialize the DynamoDB client (call once) |
85
+ | `getDocClient()` | Get the initialized Document Client |
86
+ | `getTableName()` | Get the configured table name |
87
+ | `isInitialized()` | Check if client is initialized |
88
+ | `resetClient()` | Reset client state (for testing) |
89
+
90
+ ### Constants
91
+
92
+ | Constant | Value | Description |
93
+ |----------|-------|-------------|
94
+ | `APEX` | `"@"` | Root-level scope marker (DynamoDB prohibits empty strings) |
95
+ | `SEPARATOR` | `"#"` | Composite key separator |
96
+ | `ARCHIVED_SUFFIX` | `"#archived"` | Suffix for archived entity GSI keys |
97
+ | `DELETED_SUFFIX` | `"#deleted"` | Suffix for soft-deleted entity GSI keys |
98
+
99
+ ### StorableEntity Type
100
+
101
+ All entities follow this shape:
71
102
 
72
103
  ```typescript
73
- // GSI for querying by status across all users
74
- {
75
- pk: "USER#123",
76
- sk: "ORDER#2024-01-15#abc",
77
- gsi1pk: "ORDER#pending",
78
- gsi1sk: "2024-01-15T10:00:00Z",
104
+ interface StorableEntity {
105
+ // Primary Key
106
+ model: string; // Partition key (e.g., "record", "message")
107
+ id: string; // Sort key (UUID)
108
+
109
+ // Required
110
+ name: string;
111
+ scope: string; // APEX ("@") or "{parent.model}#{parent.id}"
112
+ sequence: number; // Date.now() for chronological ordering
113
+
114
+ // Optional (trigger GSI index population when present)
115
+ alias?: string; // Human-friendly slug
116
+ category?: string; // Category for filtering
117
+ type?: string; // Type for filtering
118
+ xid?: string; // External ID for cross-system lookup
119
+
120
+ // GSI Keys (auto-populated by putEntity/updateEntity)
121
+ indexAlias?: string;
122
+ indexCategory?: string;
123
+ indexScope?: string;
124
+ indexType?: string;
125
+ indexXid?: string;
126
+
127
+ // Timestamps (ISO 8601)
128
+ createdAt: string;
129
+ updatedAt: string;
130
+ archivedAt?: string;
131
+ deletedAt?: string;
79
132
  }
80
133
  ```
81
134
 
82
- Query pending orders:
135
+ ### Entity Operations
83
136
 
84
137
  ```typescript
85
- const result = await client.send(new QueryCommand({
86
- TableName: process.env.CDK_ENV_TABLE,
87
- IndexName: "gsi1",
88
- KeyConditionExpression: "gsi1pk = :status",
89
- ExpressionAttributeValues: {
90
- ":status": "ORDER#pending",
138
+ import { APEX, putEntity, getEntity, updateEntity, deleteEntity, archiveEntity, destroyEntity } from "@jaypie/dynamodb";
139
+
140
+ const now = new Date().toISOString();
141
+
142
+ // Create entity — auto-populates GSI keys
143
+ const record = await putEntity({
144
+ entity: {
145
+ model: "record",
146
+ id: crypto.randomUUID(),
147
+ name: "Daily Log",
148
+ scope: APEX,
149
+ sequence: Date.now(),
150
+ alias: "2026-01-07",
151
+ category: "memory",
152
+ createdAt: now,
153
+ updatedAt: now,
91
154
  },
92
- }));
155
+ });
156
+ // indexScope: "@#record" (auto-populated)
157
+ // indexAlias: "@#record#2026-01-07" (auto-populated)
158
+ // indexCategory: "@#record#memory" (auto-populated)
159
+
160
+ // Get by primary key
161
+ const item = await getEntity({ id: "abc-123", model: "record" });
162
+
163
+ // Update — sets updatedAt, re-indexes
164
+ await updateEntity({ entity: { ...item, name: "Updated Name" } });
165
+
166
+ // Soft delete — sets deletedAt, re-indexes with #deleted suffix
167
+ await deleteEntity({ id: "abc-123", model: "record" });
168
+
169
+ // Archive — sets archivedAt, re-indexes with #archived suffix
170
+ await archiveEntity({ id: "abc-123", model: "record" });
171
+
172
+ // Hard delete — permanently removes
173
+ await destroyEntity({ id: "abc-123", model: "record" });
174
+ ```
175
+
176
+ | Function | Description |
177
+ |----------|-------------|
178
+ | `putEntity({ entity })` | Create or replace (auto-indexes GSI keys) |
179
+ | `getEntity({ id, model })` | Get by primary key |
180
+ | `updateEntity({ entity })` | Update (sets `updatedAt`, re-indexes) |
181
+ | `deleteEntity({ id, model })` | Soft delete (`deletedAt`, `#deleted` suffix) |
182
+ | `archiveEntity({ id, model })` | Archive (`archivedAt`, `#archived` suffix) |
183
+ | `destroyEntity({ id, model })` | Hard delete (permanent) |
184
+
185
+ ### Scope and Hierarchy
186
+
187
+ The `scope` field enables parent-child relationships:
188
+
189
+ ```typescript
190
+ import { APEX, calculateScope, putEntity, queryByScope } from "@jaypie/dynamodb";
191
+
192
+ // Root-level entity: scope = APEX ("@")
193
+ await putEntity({ entity: { model: "chat", scope: APEX, ... } });
194
+
195
+ // Child entity: scope = "{parent.model}#{parent.id}"
196
+ const chat = { model: "chat", id: "abc-123" };
197
+ const messageScope = calculateScope(chat); // "chat#abc-123"
198
+
199
+ await putEntity({
200
+ entity: { model: "message", scope: messageScope, ... },
201
+ });
202
+
203
+ // Query all messages in a chat
204
+ const { items } = await queryByScope({ model: "message", scope: messageScope });
205
+
206
+ // Query all root-level chats
207
+ const { items: chats } = await queryByScope({ model: "chat", scope: APEX });
208
+ ```
209
+
210
+ ### GSI Schema
211
+
212
+ Jaypie defines five GSI patterns, but **do not create all five upfront**. Start with zero GSIs and add only what your access patterns require. The most common first GSI is `indexScope` for hierarchical queries.
213
+
214
+ **Important:** DynamoDB allows only **one GSI to be added per deployment**. If you need multiple GSIs, add them sequentially across separate deploys. For production tables, the AWS CLI is often better suited for adding GSIs than CDK (which may try to replace the table).
215
+
216
+ All GSIs use `sequence` (Number) as the sort key for chronological ordering.
217
+
218
+ | GSI Name | Partition Key Pattern | Purpose | Add When |
219
+ |----------|----------------------|---------|----------|
220
+ | `indexScope` | `{scope}#{model}` | List entities by parent | You need hierarchical queries |
221
+ | `indexAlias` | `{scope}#{model}#{alias}` | Human-friendly slug lookup | You need slug-based lookups |
222
+ | `indexCategory` | `{scope}#{model}#{category}` | Category filtering | You need to filter by category |
223
+ | `indexType` | `{scope}#{model}#{type}` | Type filtering | You need to filter by type |
224
+ | `indexXid` | `{scope}#{model}#{xid}` | External ID lookup | You need cross-system ID lookups |
225
+
226
+ ### Query Functions
227
+
228
+ All queries return `{ items, lastEvaluatedKey }` and support pagination:
229
+
230
+ ```typescript
231
+ import { APEX, queryByScope, queryByAlias, queryByCategory, queryByType, queryByXid } from "@jaypie/dynamodb";
232
+
233
+ // List by parent scope (most common)
234
+ const { items } = await queryByScope({ model: "record", scope: APEX });
235
+
236
+ // Filter by category
237
+ const { items: memories } = await queryByCategory({
238
+ model: "record", scope: APEX, category: "memory",
239
+ });
240
+
241
+ // Lookup by alias (returns single or null)
242
+ const item = await queryByAlias({ model: "record", scope: APEX, alias: "2026-01-07" });
243
+
244
+ // Filter by type
245
+ const { items: drafts } = await queryByType({
246
+ model: "record", scope: APEX, type: "draft",
247
+ });
248
+
249
+ // Lookup by external ID (returns single or null)
250
+ const item = await queryByXid({ model: "user", scope: APEX, xid: "github:12345" });
251
+ ```
252
+
253
+ #### Query Options
254
+
255
+ ```typescript
256
+ const { items, lastEvaluatedKey } = await queryByScope({
257
+ model: "record",
258
+ scope: APEX,
259
+ limit: 10, // Max items
260
+ ascending: true, // Oldest first (default: false, newest first)
261
+ deleted: true, // Query soft-deleted entities
262
+ archived: true, // Query archived entities
263
+ });
264
+
265
+ // Pagination
266
+ const nextPage = await queryByScope({
267
+ model: "record",
268
+ scope: APEX,
269
+ startKey: lastEvaluatedKey,
270
+ });
271
+ ```
272
+
273
+ ### Seed and Export
274
+
275
+ Idempotent seeding for bootstrapping data and export for migrations:
276
+
277
+ ```typescript
278
+ import { APEX, seedEntities, seedEntityIfNotExists, exportEntities } from "@jaypie/dynamodb";
279
+
280
+ // Seed a single entity if it doesn't exist (checks by alias)
281
+ const created = await seedEntityIfNotExists({
282
+ alias: "config-main", model: "config", name: "Main Config", scope: APEX,
283
+ });
284
+
285
+ // Seed multiple entities (idempotent)
286
+ const result = await seedEntities([
287
+ { alias: "vocab-en", model: "vocabulary", name: "English", scope: APEX },
288
+ { alias: "vocab-es", model: "vocabulary", name: "Spanish", scope: APEX },
289
+ ]);
290
+ // result.created: ["vocab-en"] — aliases that were created
291
+ // result.skipped: ["vocab-es"] — aliases that already existed
292
+ // result.errors: [] — any failures
293
+
294
+ // Dry run
295
+ await seedEntities(entities, { dryRun: true });
296
+
297
+ // Replace existing
298
+ await seedEntities(entities, { replace: true });
299
+
300
+ // Export entities
301
+ const { entities, count } = await exportEntities("vocabulary", APEX);
302
+
303
+ // Export as JSON
304
+ const json = await exportEntitiesToJson("vocabulary", APEX);
305
+ ```
306
+
307
+ ### Key Builders
308
+
309
+ Build composite GSI keys manually when needed:
310
+
311
+ ```typescript
312
+ import { buildIndexScope, buildIndexAlias, buildIndexCategory, calculateScope } from "@jaypie/dynamodb";
313
+
314
+ buildIndexScope(APEX, "record"); // "@#record"
315
+ buildIndexAlias(APEX, "record", "daily-log"); // "@#record#daily-log"
316
+ buildIndexCategory(APEX, "record", "memory"); // "@#record#memory"
317
+ calculateScope({ model: "chat", id: "abc-123" }); // "chat#abc-123"
93
318
  ```
94
319
 
95
320
  ## CDK Table Definition
96
321
 
97
- Start with a basic table and add indexes only when access patterns demand them.
322
+ `JaypieDynamoDb` provides these defaults:
323
+
324
+ | Setting | Default | Source |
325
+ |---------|---------|--------|
326
+ | Partition key | `model` (String) | Jaypie construct |
327
+ | Sort key | `id` (String) | Jaypie construct |
328
+ | Billing mode | PAY_PER_REQUEST | Jaypie construct |
329
+ | Removal policy | DESTROY (non-production), RETAIN (production) | Jaypie construct |
330
+ | Point-in-time recovery | Enabled | Jaypie construct |
331
+ | Encryption | AWS-owned key | CDK default |
98
332
 
99
333
  ```typescript
100
334
  import { JaypieDynamoDb } from "@jaypie/constructs";
101
335
 
102
- // Recommended: start with no indexes
336
+ // Recommended: start with no indexes (uses model/id keys)
103
337
  const table = new JaypieDynamoDb(this, "myApp");
104
338
 
105
- // Add indexes later when driven by real access patterns
339
+ // Add indexes when driven by real access patterns
106
340
  const table = new JaypieDynamoDb(this, "myApp", {
107
341
  indexes: [
108
342
  { pk: ["scope", "model"], sk: ["sequence"] },
@@ -110,6 +344,8 @@ const table = new JaypieDynamoDb(this, "myApp", {
110
344
  });
111
345
  ```
112
346
 
347
+ Wire tables to Lambda using the `tables` prop — see `skill("cdk")` for details.
348
+
113
349
  ## Local Development
114
350
 
115
351
  Use docker-compose for local DynamoDB. The `@jaypie/dynamodb` MCP tool can generate a `docker-compose.yml` with custom ports.
@@ -128,35 +364,43 @@ Use docker-compose for local DynamoDB. The `@jaypie/dynamodb` MCP tool can gener
128
364
  }
129
365
  ```
130
366
 
131
- | Script | Description |
132
- |--------|-------------|
133
- | `dynamo:init` | Start containers and create the default table |
134
- | `dynamo:create-table` | Create the local table (idempotent) |
135
- | `dynamo:remove` | Stop containers and delete data volumes |
136
- | `dynamo:start` | Start containers (data persists) |
137
- | `dynamo:stop` | Stop containers (data persists) |
367
+ ## Raw SDK Patterns
138
368
 
139
- Adjust the `--endpoint-url` port and `--table-name` to match your `docker-compose.yml`.
369
+ For cases where you need direct SDK access instead of `@jaypie/dynamodb`:
140
370
 
141
- ### Manual Setup
371
+ ```typescript
372
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
373
+ import { QueryCommand } from "@aws-sdk/lib-dynamodb";
142
374
 
143
- ```bash
144
- # Start local DynamoDB
145
- docker run -p 8000:8000 amazon/dynamodb-local
375
+ const client = new DynamoDBClient({});
146
376
 
147
- # Create table
148
- AWS_ACCESS_KEY_ID=local AWS_SECRET_ACCESS_KEY=local \
149
- aws dynamodb create-table \
150
- --table-name MyTable \
151
- --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \
152
- --key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \
153
- --billing-mode PAY_PER_REQUEST \
154
- --endpoint-url http://127.0.0.1:8000
377
+ const result = await client.send(new QueryCommand({
378
+ TableName: process.env.CDK_ENV_TABLE,
379
+ KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)",
380
+ ExpressionAttributeValues: {
381
+ ":pk": "USER#123",
382
+ ":prefix": "ORDER#",
383
+ },
384
+ ScanIndexForward: false,
385
+ Limit: 10,
386
+ }));
155
387
  ```
156
388
 
157
389
  ## Testing
158
390
 
159
- Mock DynamoDB in tests:
391
+ Mock `@jaypie/dynamodb` via testkit. Key builders and `indexEntity` work correctly in tests:
392
+
393
+ ```typescript
394
+ import {
395
+ APEX,
396
+ indexEntity,
397
+ putEntity,
398
+ queryByScope,
399
+ seedEntities,
400
+ } from "@jaypie/testkit/mock";
401
+ ```
402
+
403
+ For raw SDK mocking:
160
404
 
161
405
  ```typescript
162
406
  import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
@@ -167,11 +411,10 @@ vi.mock("@aws-sdk/client-dynamodb");
167
411
  describe("OrderService", () => {
168
412
  it("queries user orders", async () => {
169
413
  vi.mocked(DynamoDBClient.prototype.send).mockResolvedValue({
170
- Items: [{ pk: "USER#123", sk: "ORDER#abc" }],
414
+ Items: [{ model: "order", id: "order-abc" }],
171
415
  });
172
416
 
173
417
  const orders = await getOrders("123");
174
-
175
418
  expect(orders).toHaveLength(1);
176
419
  });
177
420
  });
@@ -193,3 +436,9 @@ Version 0.4.0 renamed `class` → `category` and `indexClass` → `indexCategory
193
436
  | `INDEX_CLASS` | `INDEX_CATEGORY` |
194
437
  | `queryByClass()` | `queryByCategory()` |
195
438
 
439
+ ## See Also
440
+
441
+ - **`skill("apikey")`** - API key hash storage in DynamoDB
442
+ - **`skill("cdk")`** - JaypieDynamoDb construct and Lambda table wiring
443
+ - **`skill("models")`** - Data model and type definition patterns
444
+ - **`skill("tools-dynamodb")`** - Interactive DynamoDB MCP tools
package/skills/errors.md CHANGED
@@ -140,3 +140,9 @@ it("includes context in error", async () => {
140
140
  });
141
141
  ```
142
142
 
143
+ ## See Also
144
+
145
+ - **`skill("handlers")`** - Handler lifecycle and automatic error formatting
146
+ - **`skill("logs")`** - Logging patterns for error context
147
+ - **`skill("tests")`** - Testing error types with Vitest
148
+
package/skills/index.md CHANGED
@@ -5,3 +5,14 @@ description: Skill directory listing
5
5
  # Jaypie Skills
6
6
 
7
7
  Query the Jaypie MCP `skill` tool with one of the following alias keywords `mcp__jaypie__skill(alias: String)`:
8
+
9
+ ## Categories
10
+
11
+ | Category | Skills |
12
+ |----------|--------|
13
+ | contents | index, releasenotes |
14
+ | development | apikey, documentation, errors, llm, logs, mocks, monorepo, style, subpackages, tests |
15
+ | infrastructure | aws, cdk, cicd, datadog, dns, dynamodb, express, lambda, secrets, streaming, variables, websockets |
16
+ | patterns | fabric, handlers, models, services, vocabulary |
17
+ | recipes | recipe-api-server |
18
+ | meta | issues, jaypie, skills, tools |
package/skills/logs.md CHANGED
@@ -158,3 +158,9 @@ export const handler = lambdaHandler(async (event) => {
158
158
  });
159
159
  ```
160
160
 
161
+ ## See Also
162
+
163
+ - **`skill("datadog")`** - Datadog integration and log forwarding
164
+ - **`skill("handlers")`** - Handler lifecycle with automatic log context
165
+ - **`skill("variables")`** - LOG_LEVEL and other environment variables
166
+
package/skills/mocks.md CHANGED
@@ -172,3 +172,8 @@ export function mockJaypie(vi) {
172
172
  }
173
173
  ```
174
174
 
175
+ ## See Also
176
+
177
+ - **`skill("errors")`** - Error types used in mock assertions
178
+ - **`skill("tests")`** - Testing patterns with Vitest
179
+
package/skills/models.md CHANGED
@@ -176,6 +176,12 @@ export interface PaginatedResponse<T> {
176
176
  4. **Document fields** - Add JSDoc comments for complex types
177
177
  5. **Prefer interfaces** - Use `interface` over `type` for objects
178
178
 
179
+ ## See Also
180
+
181
+ - **`skill("dynamodb")`** - DynamoDB key conventions and table patterns
182
+ - **`skill("fabric")`** - Fabric service input schema definitions
183
+ - **`skill("services")`** - Service layer architecture patterns
184
+
179
185
  ## Type Documentation
180
186
 
181
187
  ```typescript
@@ -0,0 +1,205 @@
1
+ ---
2
+ description: Task-oriented guide for building a Jaypie API server with DynamoDB and API keys
3
+ related: apikey, cdk, dynamodb, express, secrets, variables
4
+ ---
5
+
6
+ # Recipe: API Server with DynamoDB + API Keys
7
+
8
+ Step-by-step guide for creating a Jaypie API server stack with DynamoDB storage, generated secrets, and API key authentication.
9
+
10
+ ## 1. Create the DynamoDB Table
11
+
12
+ ```typescript
13
+ import { JaypieDynamoDb } from "@jaypie/constructs";
14
+
15
+ const table = new JaypieDynamoDb(this, "myApi");
16
+ // Default keys: model (partition), id (sort)
17
+ // Billing: PAY_PER_REQUEST
18
+ // Removal: DESTROY (non-prod), RETAIN (prod)
19
+ ```
20
+
21
+ No indexes needed initially. API key hashes are stored with `model: "apikey"` and `id: <hash>`, giving direct lookup by hash without a GSI.
22
+
23
+ See `skill("dynamodb")` for key conventions and query patterns.
24
+
25
+ ## 2. Create Generated Secrets
26
+
27
+ Two secrets support API key workflows:
28
+
29
+ ```typescript
30
+ import { JaypieEnvSecret } from "@jaypie/constructs";
31
+ import { isProductionEnv } from "@jaypie/kit";
32
+
33
+ // PROJECT_SALT — HMAC salt for hashing keys before storage
34
+ // If lost, all stored key hashes become unverifiable
35
+ const salt = new JaypieEnvSecret(this, "ProjectSalt", {
36
+ envKey: "PROJECT_SALT",
37
+ generateSecretString: {
38
+ excludePunctuation: true,
39
+ includeSpace: false,
40
+ passwordLength: 64,
41
+ },
42
+ removalPolicy: isProductionEnv(),
43
+ });
44
+
45
+ // PROJECT_ADMIN_SEED — derives the bootstrap owner key deterministically
46
+ const adminSeed = new JaypieEnvSecret(this, "ProjectAdminSeed", {
47
+ envKey: "PROJECT_ADMIN_SEED",
48
+ generateSecretString: {
49
+ excludePunctuation: true,
50
+ includeSpace: false,
51
+ passwordLength: 64,
52
+ },
53
+ });
54
+ ```
55
+
56
+ **Personal environment note:** If deploying to a personal environment (`CDK_ENV_PERSONAL=true`), these generated secrets auto-detect as consumers and try to import from sandbox. Since they don't exist there, the deploy fails with `No export named env-sandbox-...`. Fix by adding `consumer: false`:
57
+
58
+ ```typescript
59
+ new JaypieEnvSecret(this, "ProjectSalt", {
60
+ envKey: "PROJECT_SALT",
61
+ consumer: false, // Create in this stack, don't import
62
+ generateSecretString: { /* ... */ },
63
+ });
64
+ ```
65
+
66
+ See `skill("secrets")` for the full provider/consumer pattern.
67
+
68
+ ## 3. Create the Lambda
69
+
70
+ ```typescript
71
+ import { JaypieExpressLambda } from "@jaypie/constructs";
72
+
73
+ new JaypieExpressLambda(this, "ApiLambda", {
74
+ code: "../api/dist",
75
+ handler: "index.handler",
76
+ tables: [table], // Auto-grants read/write, sets DYNAMODB_TABLE_NAME
77
+ secrets: [salt, adminSeed, "ANTHROPIC_API_KEY"],
78
+ });
79
+ ```
80
+
81
+ The `tables` prop:
82
+ - Calls `grantReadWriteData()` automatically
83
+ - Sets `DYNAMODB_TABLE_NAME` when exactly 1 table is passed
84
+
85
+ The `secrets` prop accepts both construct instances and string env var names.
86
+
87
+ See `skill("cdk")` for full Lambda configuration options.
88
+
89
+ ## 4. Express Routes
90
+
91
+ ### Key Generation Endpoint
92
+
93
+ ```typescript
94
+ import { expressHandler, generateJaypieKey, hashJaypieKey } from "jaypie";
95
+
96
+ app.post("/api/keys", expressHandler(async (req, res) => {
97
+ const key = generateJaypieKey({ issuer: "myapi" });
98
+ const hash = hashJaypieKey(key); // Uses process.env.PROJECT_SALT
99
+
100
+ // Store hash in DynamoDB
101
+ await dynamoClient.send(new PutItemCommand({
102
+ TableName: process.env.DYNAMODB_TABLE_NAME,
103
+ Item: {
104
+ model: { S: "apikey" },
105
+ id: { S: hash },
106
+ ownerId: { S: req.userId },
107
+ createdAt: { S: new Date().toISOString() },
108
+ },
109
+ }));
110
+
111
+ // Return plaintext key to user (only time it's visible)
112
+ return { key };
113
+ }));
114
+ ```
115
+
116
+ ### Key Validation Middleware
117
+
118
+ ```typescript
119
+ import { validateJaypieKey, hashJaypieKey, ForbiddenError } from "jaypie";
120
+
121
+ async function authenticateApiKey(req, res, next) {
122
+ const key = req.headers["x-api-key"];
123
+
124
+ if (!key || !validateJaypieKey(key, { issuer: "myapi" })) {
125
+ throw new ForbiddenError("Invalid API key");
126
+ }
127
+
128
+ const hash = hashJaypieKey(key);
129
+ const result = await dynamoClient.send(new GetItemCommand({
130
+ TableName: process.env.DYNAMODB_TABLE_NAME,
131
+ Key: { model: { S: "apikey" }, id: { S: hash } },
132
+ }));
133
+
134
+ if (!result.Item) {
135
+ throw new ForbiddenError("API key not found");
136
+ }
137
+
138
+ req.apiKeyOwner = result.Item.ownerId.S;
139
+ next();
140
+ }
141
+ ```
142
+
143
+ ### Seed-Derived Bootstrap Key
144
+
145
+ On first startup, derive the admin key from the seed:
146
+
147
+ ```typescript
148
+ import { expressHandler, generateJaypieKey, hashJaypieKey, loadEnvSecrets } from "jaypie";
149
+
150
+ export default expressHandler(handler, {
151
+ secrets: ["PROJECT_SALT", "PROJECT_ADMIN_SEED"],
152
+ setup: async () => {
153
+ // Derive the bootstrap key (deterministic — same every time)
154
+ const adminKey = generateJaypieKey({
155
+ seed: process.env.PROJECT_ADMIN_SEED,
156
+ issuer: "myapi",
157
+ });
158
+ const hash = hashJaypieKey(adminKey);
159
+
160
+ // Auto-provision if not exists
161
+ await dynamoClient.send(new PutItemCommand({
162
+ TableName: process.env.DYNAMODB_TABLE_NAME,
163
+ Item: {
164
+ model: { S: "apikey" },
165
+ id: { S: hash },
166
+ ownerId: { S: "admin" },
167
+ createdAt: { S: new Date().toISOString() },
168
+ },
169
+ ConditionExpression: "attribute_not_exists(id)",
170
+ })).catch(() => {}); // Ignore if already exists
171
+ },
172
+ });
173
+ ```
174
+
175
+ See `skill("apikey")` for full key generation/validation/hashing documentation.
176
+
177
+ ## 5. Deploy
178
+
179
+ ```bash
180
+ # First deploy creates secrets and table
181
+ PROJECT_ENV=sandbox PROJECT_NONCE=dev npx cdk deploy
182
+ ```
183
+
184
+ No workflow secrets needed for generated values — CloudFormation creates them on first deploy and preserves them on subsequent deploys.
185
+
186
+ ## Jaypie Defaults vs CDK Defaults
187
+
188
+ | Setting | Value | Source |
189
+ |---------|-------|--------|
190
+ | DynamoDB keys: `model`/`id` | Jaypie construct | Jaypie |
191
+ | DynamoDB billing: PAY_PER_REQUEST | Jaypie construct | Jaypie |
192
+ | DynamoDB removal: env-aware | Jaypie construct | Jaypie |
193
+ | DynamoDB PITR: enabled | Jaypie construct | Jaypie |
194
+ | DynamoDB encryption: AWS-owned | CDK default | CDK |
195
+ | Secret consumer auto-detect | Jaypie construct | Jaypie |
196
+ | Lambda `DYNAMODB_TABLE_NAME` | Jaypie construct (1 table) | Jaypie |
197
+
198
+ ## See Also
199
+
200
+ - **`skill("apikey")`** - Key generation, validation, hashing, and seed options
201
+ - **`skill("cdk")`** - CDK constructs, tables, and secrets integration
202
+ - **`skill("dynamodb")`** - Key conventions, queries, and local development
203
+ - **`skill("express")`** - Express handler and Lambda adapter
204
+ - **`skill("secrets")`** - Secret management and personal environment gotchas
205
+ - **`skill("variables")`** - Environment variables reference
package/skills/secrets.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Secret management with AWS Secrets Manager
3
- related: aws, cdk, variables
3
+ related: apikey, aws, cdk, variables
4
4
  ---
5
5
 
6
6
  # Secret Management
@@ -88,7 +88,49 @@ new JaypieEnvSecret(this, "SHARED_API_KEY", { provider: true });
88
88
  new JaypieEnvSecret(this, "SHARED_API_KEY"); // consumer auto-detected
89
89
  ```
90
90
 
91
- The construct auto-detects consumer mode for personal/ephemeral environments (`PROJECT_ENV=personal` or `CDK_ENV_PERSONAL=true`).
91
+ ### Auto-Detection
92
+
93
+ The construct auto-detects consumer mode when any of these conditions is true:
94
+
95
+ | Condition | Trigger |
96
+ |-----------|---------|
97
+ | `PROJECT_ENV=personal` | Personal environment |
98
+ | `CDK_ENV_PERSONAL=true` | Personal environment flag |
99
+ | `CDK_ENV_EPHEMERAL=true` | Legacy ephemeral flag |
100
+
101
+ When consumer mode is active, the construct **imports** a secret from the sandbox stack via CloudFormation exports instead of creating a new one. The export name follows the pattern: `env-sandbox-{PROJECT_KEY}-{SecretName}`.
102
+
103
+ ### Personal Environment Gotcha
104
+
105
+ **When adding new generated secrets in a personal environment**, the consumer auto-detection can cause deploy failures:
106
+
107
+ ```
108
+ No export named env-sandbox-myproject-ProjectSalt found
109
+ ```
110
+
111
+ This happens because the personal stack tries to import from sandbox, but sandbox has no such export yet (the secret is new).
112
+
113
+ **Fix**: Set `consumer: false` for secrets that should be created per-stack:
114
+
115
+ ```typescript
116
+ new JaypieEnvSecret(this, "ProjectSalt", {
117
+ envKey: "PROJECT_SALT",
118
+ consumer: false, // Create in this stack, don't import from sandbox
119
+ generateSecretString: {
120
+ excludePunctuation: true,
121
+ includeSpace: false,
122
+ passwordLength: 64,
123
+ },
124
+ });
125
+ ```
126
+
127
+ **When to use `consumer: false`:**
128
+ - Generated secrets (`generateSecretString`) that don't need to be shared
129
+ - Secrets unique to each environment (salts, seeds)
130
+
131
+ **When to let consumer auto-detect (default):**
132
+ - Shared vendor credentials (API keys, database URIs) that exist in sandbox
133
+ - Secrets that must be identical across sandbox and personal stacks
92
134
 
93
135
  ## Generated Secrets
94
136
 
@@ -274,6 +316,12 @@ secret.grantRead(lambdaFunction);
274
316
 
275
317
  Or use the `secrets` array on `JaypieLambda`, which handles permissions automatically.
276
318
 
319
+ ## See Also
320
+
321
+ - **`skill("apikey")`** - API key infrastructure using PROJECT_SALT and PROJECT_ADMIN_SEED
322
+ - **`skill("cdk")`** - CDK constructs including JaypieLambda secrets integration
323
+ - **`skill("variables")`** - Environment variables reference
324
+
277
325
  ## Testing
278
326
 
279
327
  Mock secret functions in tests:
@@ -173,3 +173,9 @@ describe("getUser", () => {
173
173
  });
174
174
  ```
175
175
 
176
+ ## See Also
177
+
178
+ - **`skill("fabric")`** - Fabric service pattern for multi-platform deployment
179
+ - **`skill("handlers")`** - Handler lifecycle and integration with services
180
+ - **`skill("models")`** - Data model and type definitions
181
+
package/skills/skills.md CHANGED
@@ -19,4 +19,5 @@ Look up skills by alias: `mcp__jaypie__skill(alias)`
19
19
  | development | apikey, documentation, errors, llm, logs, mocks, monorepo, style, subpackages, tests |
20
20
  | infrastructure | aws, cdk, cicd, datadog, dns, dynamodb, express, lambda, secrets, streaming, variables, websockets |
21
21
  | patterns | fabric, handlers, models, services, vocabulary |
22
+ | recipes | recipe-api-server |
22
23
  | meta | issues, jaypie, skills, tools |
package/skills/style.md CHANGED
@@ -165,3 +165,9 @@ export default [...jaypie];
165
165
  ```
166
166
 
167
167
  Always run `npm run format` before committing.
168
+
169
+ ## See Also
170
+
171
+ - **`skill("documentation")`** - Writing style for Jaypie docs
172
+ - **`skill("errors")`** - Error handling conventions
173
+ - **`skill("tests")`** - Testing patterns and conventions
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Environment variables reference
3
- related: secrets, cdk, datadog
3
+ related: apikey, cdk, datadog, secrets
4
4
  ---
5
5
 
6
6
  # Environment Variables
@@ -15,6 +15,8 @@ Configuration variables used in Jaypie applications.
15
15
  | `PROJECT_KEY` | Project name for logging | e.g., my-api |
16
16
  | `PROJECT_NONCE` | Unique resource suffix | e.g., dev, staging, prod |
17
17
  | `PROJECT_CHAOS` | Chaos engineering mode | none, partial, full |
18
+ | `PROJECT_SALT` | Secret salt for HMAC hashing (API keys) | Generated by JaypieEnvSecret |
19
+ | `PROJECT_ADMIN_SEED` | Secret seed for deterministic key derivation | Generated by JaypieEnvSecret |
18
20
 
19
21
  ### Usage
20
22
 
@@ -52,6 +54,8 @@ LOG_LEVEL=debug npm run dev
52
54
  | `CDK_ENV_SNS_TOPIC_ARN` | SNS topic ARN |
53
55
  | `CDK_ENV_DATADOG_API_KEY_ARN` | Datadog API key ARN |
54
56
  | `CDK_ENV_TABLE` | DynamoDB table name |
57
+ | `CDK_ENV_PERSONAL` | Personal environment flag (triggers consumer mode for secrets) |
58
+ | `DYNAMODB_TABLE_NAME` | Auto-set by JaypieLambda when exactly 1 table is passed |
55
59
 
56
60
  ### Passing to Lambda
57
61
 
@@ -113,6 +117,13 @@ environment: {
113
117
  }
114
118
  ```
115
119
 
120
+ ## Application Variables
121
+
122
+ | Variable | Description |
123
+ |----------|-------------|
124
+ | `BASE_URL` | Application base URL (used by CORS) |
125
+ | `PROJECT_BASE_URL` | Project base URL (used by CORS) |
126
+
116
127
  ## Local Development
117
128
 
118
129
  ### .env Files
@@ -144,3 +155,10 @@ const isLocal = process.env.PROJECT_ENV === "local";
144
155
  const isDevelopment = !isProduction;
145
156
  ```
146
157
 
158
+ ## See Also
159
+
160
+ - **`skill("apikey")`** - Uses PROJECT_SALT and PROJECT_ADMIN_SEED
161
+ - **`skill("cdk")`** - CDK constructs that set infrastructure variables
162
+ - **`skill("cors")`** - Uses BASE_URL and PROJECT_BASE_URL
163
+ - **`skill("secrets")`** - Secret management and CDK_ENV_PERSONAL behavior
164
+