This guide covers everything you need to know about using NPM packages in Supabase Edge Functions, which run on a Deno-based runtime with full NPM compatibility.
I write these posts while creating LLM context for myself, double-checking to make sure they're hallucination-free. I'm adding them as search context for both LLMs and people working in niche fields — plus, I need them anyway. They weren't written entirely by me; they were created through my guidance combined with extensive deep research, merged with long-context LLMs. Just dropping this note here 🖖🏾
Table of Contents
- Understanding NPM Compatibility
- Importing NPM Packages
- Managing Dependencies
- Working with Node.js Built-ins
- Private NPM Packages
- TypeScript Support
- Local Development Workflow
- Deployment
- Best Practices
- Common Patterns & Examples
- Troubleshooting
Understanding NPM Compatibility
Supabase Edge Functions run on the Supabase Edge Runtime, a Deno-compatible runtime that natively supports:
- ✅ NPM packages directly via
npm:specifier - ✅ Node.js built-in APIs via
node:specifier - ✅ JSR modules via
jsr:specifier - ✅ Deno modules from
deno.land/x - ✅ TypeScript first
- ✅ Global distribution
Key Benefits:
- Access to millions of NPM packages
- No build step required for imports
- Code reusability from Node.js projects
- Familiar Node.js APIs available
Importing NPM Packages
Method 1: Direct Import (Simplest)
You can import NPM packages directly using the npm: specifier:
// Import with version
import express from 'npm:express@4.18.2'
import { drizzle } from 'npm:drizzle-orm@0.29.0/node-postgres'
// Import latest version (not recommended for production)
import lodash from 'npm:lodash@latest'
// Import specific submodules
import { Pool } from 'npm:pg@8.11.3'
Method 2: Using deno.json (Recommended)
Create a deno.json file in your function directory for better dependency management:
└── supabase
└── functions
└── my-function
├── index.ts
└── deno.json
deno.json:
{
"imports": {
"supabase": "npm:@supabase/supabase-js@2",
"express": "npm:express@4.18.2",
"pg": "npm:pg@8.11.3",
"drizzle-orm": "npm:drizzle-orm@0.29.0",
"drizzle-orm/": "npm:drizzle-orm@0.29.0/",
"stripe": "npm:stripe@14.0.0"
}
}
Then import in your code:
// index.ts
import { createClient } from 'supabase'
import express from 'express'
import { Pool } from 'pg'
import { drizzle } from 'drizzle-orm/node-postgres'
import Stripe from 'stripe'
Note the trailing slash for submodules:
{
"imports": {
"drizzle-orm": "npm:drizzle-orm@0.29.0",
"drizzle-orm/": "npm:drizzle-orm@0.29.0/"
}
}
This allows both:
import { drizzle } from 'drizzle-orm' // Main module
import { pgTable } from 'drizzle-orm/pg-core' // Submodule
Managing Dependencies
Per-Function Isolation (Recommended)
Each function should have its own deno.json:
└── supabase
├── functions
│ ├── stripe-webhook
│ │ ├── index.ts
│ │ └── deno.json
│ ├── send-email
│ │ ├── index.ts
│ │ └── deno.json
│ └── process-image
│ ├── index.ts
│ └── deno.json
└── config.toml
Benefits:
- Version isolation (no conflicts between functions)
- Independent deployments
- Clear dependency tracking per function
Legacy: Import Maps (import_map.json)
While still supported, import_map.json is legacy. If both exist, deno.json takes precedence.
{
"imports": {
"lodash": "https://cdn.skypack.dev/lodash",
"stripe": "npm:stripe@14.0.0"
}
}
Working with Node.js Built-ins
Deno supports Node.js built-in APIs via the node: specifier:
// Built-in Node APIs
import process from 'node:process'
import { readFile } from 'node:fs/promises'
import crypto from 'node:crypto'
import { Buffer } from 'node:buffer'
import path from 'node:path'
import { EventEmitter } from 'node:events'
// Example usage
Deno.serve(async (req) => {
const env = process.env.MY_SECRET
const hash = crypto.createHash('sha256').update('data').digest('hex')
return new Response(JSON.stringify({ hash }))
})
Common Pattern: Migrating Node.js Code
Original Node.js code:
const express = require('express')
const { Pool } = require('pg')
const process = require('process')
const pool = new Pool({
connectionString: process.env.DATABASE_URL
})
Migrated to Supabase Edge Function:
import express from 'npm:express@4.18.2'
import { Pool } from 'npm:pg@8.11.3'
import process from 'node:process'
const pool = new Pool({
connectionString: process.env.DATABASE_URL
})
Or use deno.json:
{
"imports": {
"express": "npm:express@4.18.2",
"pg": "npm:pg@8.11.3",
"process": "node:process"
}
}
Private NPM Packages
Setup
Requirements:
- Supabase CLI version 1.207.9 or higher
Step 1: Create .npmrc File
Create .npmrc in your function directory:
└── supabase
└── functions
└── my-function
├── index.ts
├── deno.json
└── .npmrc
Step 2: Configure Registry
.npmrc:
# Scoped package from private registry
@myorg:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
# Or for npm registry
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
# Multiple scopes
@mycompany:registry=https://npm.mycompany.com
//npm.mycompany.com/:_authToken=${COMPANY_TOKEN}
Step 3: Set Environment Variable
Local development (.env):
NPM_TOKEN=ghp_your_github_token_here
COMPANY_TOKEN=your_company_token_here
Production:
# Set secrets via CLI
supabase secrets set NPM_TOKEN=ghp_your_token
# Or via Dashboard
# Go to Edge Functions → Secrets → Add Secret
Step 4: Import Private Package
deno.json:
{
"imports": {
"my-private-lib": "npm:@myorg/private-package@1.0.0"
}
}
index.ts:
import { myFunction } from 'my-private-lib'
Custom NPM Registry
Requirements:
- Supabase CLI version 2.2.8 or higher
Set custom registry:
# Via environment variable
NPM_CONFIG_REGISTRY=https://custom-registry.com/ supabase functions deploy my-function
# Or in .env
NPM_CONFIG_REGISTRY=https://custom-registry.com/
TypeScript Support
Automatic Type Inference
Most NPM packages with types work automatically:
import { createClient } from 'npm:@supabase/supabase-js@2'
const supabase = createClient(url, key)
// TypeScript types are automatically available
Adding Types for Packages Without Them
Some packages need separate type definitions:
// Use @deno-types directive
// @deno-types="npm:@types/express@^4.17"
import express from 'npm:express@^4.17'
const app = express()
// Now has full TypeScript support
Node.js Built-in Types
/// <reference types="npm:@types/node" />
import process from 'node:process'
import { EventEmitter } from 'node:events'
// Now has full Node.js type definitions
In deno.json
{
"compilerOptions": {
"types": ["npm:@types/node"]
},
"imports": {
"express": "npm:express@4.18.2"
}
}
Local Development Workflow
Step 1: Initialize Supabase
# Install Supabase CLI
npm install supabase@latest --save-dev
# Initialize project
npx supabase init
Step 2: Create Edge Function
npx supabase functions new my-function
This creates:
└── supabase
└── functions
└── my-function
└── index.ts
Step 3: Add Dependencies
Create deno.json:
{
"imports": {
"stripe": "npm:stripe@14.0.0",
"supabase": "npm:@supabase/supabase-js@2"
}
}
Step 4: Write Function
index.ts:
import Stripe from 'stripe'
import { createClient } from 'supabase'
Deno.serve(async (req) => {
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') ?? '', {
apiVersion: '2023-10-16'
})
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!
)
// Your logic here
const customer = await stripe.customers.create({
email: 'test@example.com'
})
return new Response(
JSON.stringify({ customerId: customer.id }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
Step 5: Set Up Environment
supabase/functions/.env:
STRIPE_SECRET_KEY=sk_test_...
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
Step 6: Serve Locally
# Start Supabase (includes local Postgres, Auth, etc.)
npx supabase start
# Serve function
npx supabase functions serve my-function
# With custom env file
npx supabase functions serve my-function --env-file .env.local
# Without JWT verification (for testing)
npx supabase functions serve my-function --no-verify-jwt
Step 7: Test
curl --request POST \
'http://localhost:54321/functions/v1/my-function' \
--header 'Authorization: Bearer YOUR_ANON_KEY' \
--header 'Content-Type: application/json' \
--data '{"test": true}'
Deployment
Deploy Single Function
npx supabase functions deploy my-function
Deploy All Functions
npx supabase functions deploy
Deploy with Environment Variables
# Set secrets before deploying
npx supabase secrets set STRIPE_SECRET_KEY=sk_live_...
# Or set multiple at once
npx supabase secrets set --env-file .env.production
Verify Deployment
# List deployed functions
npx supabase functions list
# Check logs
npx supabase functions logs my-function
# Invoke production function
npx supabase functions invoke my-function \
--body '{"test": true}'
Best Practices
1. Always Pin Versions
❌ Bad:
{
"imports": {
"express": "npm:express" // Unpredictable
}
}
✅ Good:
{
"imports": {
"express": "npm:express@4.18.2" // Explicit version
}
}
2. Use deno.json per Function
✅ Recommended structure:
functions/
├── stripe-webhook/
│ ├── index.ts
│ └── deno.json
├── send-email/
│ ├── index.ts
│ └── deno.json
3. Handle Node.js Globals Properly
// ❌ Won't work - process is not a global in Deno
const secret = process.env.SECRET
// ✅ Import explicitly
import process from 'node:process'
const secret = process.env.SECRET
// ✅ Or use Deno's native API
const secret = Deno.env.get('SECRET')
4. Error Handling
Deno.serve(async (req) => {
try {
// Your logic
return new Response(JSON.stringify({ success: true }))
} catch (error) {
console.error('Function error:', error)
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500 }
)
}
})
5. Environment Variables
// ✅ Always provide defaults or handle missing vars
const apiKey = Deno.env.get('API_KEY') ?? ''
if (!apiKey) {
throw new Error('API_KEY is required')
}
// Or use nullish coalescing with error
const url = Deno.env.get('SUPABASE_URL')! // Asserts non-null
6. Private Packages Security
# ❌ Don't commit .npmrc with tokens
echo ".npmrc" >> .gitignore
# ✅ Use environment variables in .npmrc
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
Common Patterns & Examples
Example 1: Express Server Migration
Original Node.js app:
// index.js (Node.js)
const express = require('express')
const { Pool } = require('pg')
const app = express()
const pool = new Pool({
connectionString: process.env.DATABASE_URL
})
app.get('/notes', async (req, res) => {
const result = await pool.query('SELECT * FROM notes')
res.json(result.rows)
})
app.listen(3000)
Supabase Edge Function:
deno.json:
{
"imports": {
"express": "npm:express@4.18.2",
"pg": "npm:pg@8.11.3",
"process": "node:process"
}
}
index.ts:
import express from 'express'
import { Pool } from 'pg'
import process from 'process'
const app = express()
const pool = new Pool({
connectionString: process.env.DATABASE_URL
})
app.get('/notes', async (req, res) => {
const result = await pool.query('SELECT * FROM notes')
res.json(result.rows)
})
// Wrap Express in Deno.serve
Deno.serve(app.fetch)
Example 2: Stripe Webhook Handler
deno.json:
{
"imports": {
"stripe": "npm:stripe@14.0.0"
}
}
index.ts:
import Stripe from 'stripe'
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, {
apiVersion: '2023-10-16'
})
Deno.serve(async (req) => {
const signature = req.headers.get('stripe-signature')!
const body = await req.text()
try {
const event = stripe.webhooks.constructEvent(
body,
signature,
Deno.env.get('STRIPE_WEBHOOK_SECRET')!
)
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object
console.log('Payment succeeded:', paymentIntent.id)
break
// Handle other events
}
return new Response(JSON.stringify({ received: true }), {
headers: { 'Content-Type': 'application/json' }
})
} catch (err) {
return new Response(
JSON.stringify({ error: err.message }),
{ status: 400 }
)
}
})
config.toml:
[functions.stripe-webhook]
verify_jwt = false # Stripe doesn't send JWT
Example 3: Send Email with Resend
deno.json:
{
"imports": {
"resend": "npm:resend@2.0.0"
}
}
index.ts:
import { Resend } from 'resend'
const resend = new Resend(Deno.env.get('RESEND_API_KEY'))
Deno.serve(async (req) => {
const { to, subject, html } = await req.json()
const data = await resend.emails.send({
from: 'onboarding@resend.dev',
to,
subject,
html
})
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' }
})
})
Example 4: Using Drizzle ORM
deno.json:
{
"imports": {
"drizzle-orm": "npm:drizzle-orm@0.29.0",
"drizzle-orm/": "npm:drizzle-orm@0.29.0/",
"pg": "npm:pg@8.11.3"
}
}
schema.ts:
import { pgTable, serial, text } from 'drizzle-orm/pg-core'
export const notes = pgTable('notes', {
id: serial('id').primaryKey(),
title: text('title').notNull()
})
index.ts:
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import { notes } from './schema.ts'
const pool = new Pool({
connectionString: Deno.env.get('DATABASE_URL')
})
const db = drizzle(pool)
Deno.serve(async (req) => {
// Get all notes
const allNotes = await db.select().from(notes)
return new Response(JSON.stringify(allNotes), {
headers: { 'Content-Type': 'application/json' }
})
})
Example 5: OpenAI Integration
deno.json:
{
"imports": {
"openai": "npm:openai@4.20.0"
}
}
index.ts:
import OpenAI from 'openai'
const openai = new OpenAI({
apiKey: Deno.env.get('OPENAI_API_KEY')
})
Deno.serve(async (req) => {
const { prompt } = await req.json()
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }]
})
return new Response(
JSON.stringify({ response: completion.choices[0].message.content }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
Troubleshooting
Issue 1: "Module not found" Error
Problem:
error: Module not found "https://deno.land/x/..."
Solution:
// ✅ Use npm: prefix for NPM packages
import lodash from 'npm:lodash@4.17.21'
// Not:
// ❌ import lodash from 'lodash'
Issue 2: Process is Not Defined
Problem:
error: process is not defined
Solution:
// ✅ Import Node.js globals
import process from 'node:process'
// Or use Deno API
const secret = Deno.env.get('SECRET')
Issue 3: Submodule Import Fails
Problem:
import { pgTable } from 'drizzle-orm/pg-core' // Error
Solution:
{
"imports": {
"drizzle-orm": "npm:drizzle-orm@0.29.0",
"drizzle-orm/": "npm:drizzle-orm@0.29.0/" // Note trailing slash
}
}
Issue 4: Private Package 401 Unauthorized
Problem:
error: npm package '@myorg/private-pkg' could not be downloaded. 401 Unauthorized
Solution:
- Check
.npmrcsyntax - Verify token is set:
echo $NPM_TOKEN - Test token:
npm whoami --registry=https://your-registry - Ensure token in secrets:
supabase secrets list
Issue 5: Function Timeout
Problem:
Large dependencies causing cold start timeout.
Solution:
- Minimize dependencies
- Use smaller alternatives
- Consider lazy loading:
Deno.serve(async (req) => {
// Only import when needed
const { default: heavyLib } = await import('npm:heavy-library@1.0.0')
// Use library
})
Issue 6: Type Errors
Problem:
error: Type 'any' is not assignable to type 'string'
Solution:
// Add type definitions
// @deno-types="npm:@types/express@^4.17"
import express from 'npm:express@^4.17'
// Or add to deno.json
{
"compilerOptions": {
"types": ["npm:@types/node", "npm:@types/express"]
}
}
Quick Reference
Import Syntax Cheat Sheet
// NPM packages
import pkg from 'npm:package-name@1.0.0'
// NPM with submodule
import { sub } from 'npm:package@1.0.0/submodule'
// Node.js built-ins
import fs from 'node:fs'
import process from 'node:process'
// JSR (Deno registry)
import mod from 'jsr:@std/path@1.0.0'
// Deno.land/x
import mod from 'https://deno.land/x/mod@1.0.0/mod.ts'
CLI Commands Cheat Sheet
# Initialize
npx supabase init
npx supabase functions new my-function
# Local development
npx supabase start
npx supabase functions serve
npx supabase functions serve --no-verify-jwt
# Secrets management
npx supabase secrets set KEY=value
npx supabase secrets set --env-file .env
npx supabase secrets list
# Deployment
npx supabase functions deploy my-function
npx supabase functions deploy # Deploy all
# Monitoring
npx supabase functions logs my-function
npx supabase functions list
File Structure Template
your-project/
├── supabase/
│ ├── functions/
│ │ ├── function-one/
│ │ │ ├── index.ts
│ │ │ ├── deno.json
│ │ │ └── .npmrc (if private packages)
│ │ └── function-two/
│ │ ├── index.ts
│ │ └── deno.json
│ ├── config.toml
│ └── .env (local secrets, gitignored)
└── .gitignore
Summary
You now have a comprehensive understanding of using NPM packages on Supabase's Deno runtime:
- ✅ Import directly using
npm:specifier with versions - ✅ Manage dependencies with
deno.jsonper function - ✅ Use Node.js APIs with
node:specifier - ✅ Work with private packages via
.npmrc - ✅ Get TypeScript support automatically or via
@deno-types - ✅ Develop locally with
supabase functions serve - ✅ Deploy easily with
supabase functions deploy
The Supabase Deno runtime makes it incredibly easy to use the entire NPM ecosystem while maintaining the security and performance benefits of Deno. Happy building! 🚀
Top comments (0)