Blob Storage
Netlify Blobs Storage
This document describes the Netlify Blobs storage implementation for notify subscriptions and reservations.
Overview
We use Netlify Blobs as a simple key-value store for persisting subscription and reservation data. It’s a serverless storage solution that integrates seamlessly with Netlify Functions.
Storage Architecture
Store Names
Data is stored in a single unified store with context-based separation:
| Context | Store Name | Purpose |
|---|---|---|
Production (main branch) | preorder-data | Real customer data |
| Preview/Development | preorder-data-preview | Test data (isolated from production) |
The store name is determined by the CONTEXT environment variable provided by Netlify:
const STORE_NAME =
process.env.CONTEXT === 'production' ? 'preorder-data' : 'preorder-data-preview';
This separation ensures that testing on preview deploys never affects production data.
Key Patterns
All data is stored in the unified store with key prefixes:
notify:{productSlug}:{id} # Individual notify subscription
index:notify:{productSlug} # Index of subscription IDs per product
index:products:notify # List of products with subscriptions
reservation:{productSlug}:{id} # Individual reservation
index:reservation:{productSlug} # Index of reservation IDs per product
index:products:reservation # List of products with reservations
Data Types
NotifySubscription
interface NotifySubscription {
id: string; // UUID
email: string; // Normalized to lowercase
productSlug: string; // Product identifier
createdAt: string; // ISO timestamp
notifiedAt?: string; // ISO timestamp (when notified)
status: 'pending' | 'notified' | 'unsubscribed';
}
Reservation
interface Reservation {
id: string; // Format: res_{timestamp}-{random}
name: string; // Customer name
email: string; // Normalized to lowercase
productSlug: string; // Product identifier
createdAt: string; // ISO timestamp
status: 'pending' | 'confirmed' | 'cancelled' | 'fulfilled';
notes?: string; // Admin notes
}
Available Operations
Notify Subscriptions
| Function | Description |
|---|---|
saveNotifySubscription(email, productSlug) | Create new subscription |
getNotifySubscription(productSlug, id) | Get subscription by ID |
listNotifySubscriptions(productSlug) | List all subscriptions for a product |
updateNotifyStatus(productSlug, id, status) | Update subscription status |
markAsNotified(productSlug, ids) | Bulk mark as notified |
deleteNotifySubscription(productSlug, id) | Delete a subscription |
isEmailSubscribed(productSlug, email) | Check for duplicate (case-insensitive) |
Reservations
| Function | Description |
|---|---|
saveReservation(id, name, email, productSlug) | Create new reservation |
getReservation(productSlug, id) | Get reservation by ID |
listReservations(productSlug) | List all reservations for a product |
updateReservationStatus(productSlug, id, status, notes?) | Update reservation status |
deleteReservation(productSlug, id) | Delete a reservation |
Product Indexes
| Function | Description |
|---|---|
listNotifyProducts() | List products with notify subscriptions |
listReservationProducts() | List products with reservations |
listAllProducts() | List all products (combined, sorted) |
Development Commands
| Command | Storage Location | Use Case |
|---|---|---|
pnpm dev:full | Local (.netlify/blobs-serve/) | Next.js + local functions (offline blobs) |
pnpm functions:serve | Local (.netlify/blobs-serve/) | Standalone functions server (port 9999) |
Local development with pnpm dev:full or pnpm functions:serve always uses offline/sandbox blob storage. It is not possible to access remote Netlify Blobs from local development due to Netlify’s security design. To test with real remote storage, deploy to a preview branch.
Testing with Offline Local Server
Step 1: Start Dev Server with Local API
pnpm dev:full
This starts the Netlify Functions server on port 9999 with offline blob storage plus the Next.js dev server on port 34434 that proxies /api/* requests to the functions server. Data is stored locally in .netlify/blobs-serve/ directory, completely isolated from production.
Step 2: Open Test Page
Navigate to: http://zmod.localhost:34434/test-notify-dialogs
This test page provides:
- NotifyMe Dialog - Test restock notification signup
- Reservation Dialog - Test product reservation
Step 3: Test the Dialogs
- Click “Open NotifyMe Dialog” or “Open Reservation Dialog”
- Fill in the form fields
- Submit and verify success/error responses
Step 4: Verify Stored Data
Check the local blob storage directory:
# List all stored blobs
ls -la .netlify/blobs-serve/
# View specific store contents (local dev uses preorder-data-preview)
cat .netlify/blobs-serve/preorder-data-preview/*
Example stored data structure:
.netlify/blobs-serve/
└── preorder-data-preview/
├── notify:test-product:uuid-1234
├── index:notify:test-product
├── index:products:notify
├── reservation:test-product:res_xxx
├── index:reservation:test-product
└── index:products:reservation
Step 5: Clean Up Test Data
# Remove all local blob data
rm -rf .netlify/blobs-serve/*
Testing on Preview Deployments
To test with real remote blob storage, deploy to a preview branch:
- Push changes to a preview branch (
previeworexpreview/{topic-name}) - Wait for Netlify to deploy
- Access the preview URL (e.g.,
https://*--takazudomodular.netlify.app) - Test the dialogs - data will be stored in
preorder-data-previewstore
Verify Remote Data via CLI
# List all keys in the preview store
netlify blob:list preorder-data-preview
# Get a specific key
netlify blob:get preorder-data-preview "notify:product-slug:uuid"
Remote blob data on preview deployments persists across deploys. Clean up test data manually if needed using netlify blob:delete.
Test Scenarios
| Scenario | Expected Result |
|---|---|
| Submit valid email | Success message, data stored |
| Submit same email twice | ”既に登録されています” error |
| Submit reservation | Success with reservation ID |
| Empty form submission | Validation prevents submit |
Automated Testing
Unit Tests (Mocked)
pnpm test:unit
Unit tests use Jest mocks for @netlify/blobs, testing business logic without real storage.
E2E Tests (Offline Blobs)
pnpm test:e2e:netlify
E2E tests use Playwright with the offline Netlify dev server, testing the full user flow from UI to storage.
Implementation Files
| File | Purpose |
|---|---|
netlify/functions/shared/blob-store.ts | Core CRUD operations |
netlify/functions/shared/types.ts | TypeScript interfaces |
netlify/functions/notify-signup.ts | Notify signup endpoint |
netlify/functions/reservation.ts | Reservation endpoint |
tests/unit/blob-store.test.ts | Unit tests |
tests/e2e/notify-dialogs.spec.ts | E2E tests |
Index Management
Indexes are automatically maintained when creating/deleting records:
- Product Index: Lists all IDs for a product
- Global Index: Lists all products with data
This enables efficient listing without scanning all keys.
Example: Listing Subscriptions
// 1. Get index for product
const index = await store.get('index:notify:product-abc', { type: 'json' });
// { ids: ['uuid-1', 'uuid-2', 'uuid-3'], updatedAt: '...' }
// 2. Fetch each subscription
const subscriptions = await Promise.all(
index.ids.map(id => store.get(`notify:product-abc:${id}`, { type: 'json' }))
);
Email Handling
Normalization
Both notify subscriptions and reservations normalize emails to lowercase before storage. This ensures case-insensitive handling (e.g., User@Example.com → user@example.com).
Duplicate Prevention (Notify Only)
Email duplicates are prevented for notify subscriptions:
isEmailSubscribed()checks for existing pending subscriptions- Only
pendingstatus counts as “subscribed” (allows re-subscribe after notification) - Returns error if email already subscribed to a product
Reservations Allow Duplicates
Multiple reservations from the same email are allowed by design:
- Customers may want to reserve multiple units
- Each reservation gets a unique ID for tracking
- No duplicate checking is performed
Admin Access Architecture
The Challenge
Local development (via netlify dev) cannot access remote Netlify Blobs. This is a security limitation by design. This creates challenges for building admin tools that need to view and manage production/preview data.
Solution: Admin API Endpoints
To enable admin access from external tools (like the zpreorder sub-app), we expose blob data through authenticated API endpoints:
┌─────────────────────────────────────────────────────────┐
│ Netlify (Deployed) │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ Main Site │ │ Netlify Blobs │ │
│ │ /api/admin/* │◄──►│ preorder-data │ │
│ │ (with auth) │ │ preorder-data-preview │ │
│ └────────▲─────────┘ └──────────────────────────┘ │
└───────────┼─────────────────────────────────────────────┘
│ HTTPS + PREORDER_API_TOKEN
│
┌───────────┼─────────────────────────────────────────────┐
│ Local │ │
│ ┌────────▼─────────┐ │
│ │ zpreorder│ Fetches from deployed preview │
│ │ Sub-App │ using auth token │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
Admin API Endpoints (Planned)
| Endpoint | Method | Purpose |
|---|---|---|
/api/admin/notify/list | POST | List all notify subscriptions |
/api/admin/reservations/list | POST | List all reservations |
/api/admin/notify/[id]/status | POST | Update subscription status |
/api/admin/reservations/[id]/status | POST | Update reservation status |
/api/admin/stats | POST | Get summary statistics |
Authentication
Admin endpoints require the PREORDER_API_TOKEN header (a dedicated token for the preorder API, not Netlify’s PAT):
const response = await fetch('https://preview--takazudomodular.netlify.app/api/admin/notify/list', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.PREORDER_API_TOKEN}`,
},
});
Development Modes for Admin Tools
| Mode | Data Source | Use Case |
|---|---|---|
| Mock | Local JSON files | UI development, fast iteration |
| Remote | Deployed preview API | Testing with real data |
The zpreorder sub-app supports both modes via environment variable:
# Remote mode (in sub-packages/zpreorder/.env)
VITE_BLOB_API_URL=https://preview--takazudomodular.netlify.app/api/admin
VITE_PREORDER_API_TOKEN=your-token-here
Notes
- Netlify Blobs is a key-value store, not a relational database
- No query capabilities - must fetch by exact key or use indexes
- Suitable for small to medium datasets (< 10,000 items per “table”)
- Data persists across deploys and function invocations
- Store separation: Production uses
preorder-data, preview/dev usespreorder-data-preview - Local development: Always uses offline sandbox mode (cannot access remote blobs)
- Index race conditions: Index updates are not atomic; acceptable for low-traffic usage