DEV Community

Cover image for Using NPM Packages on Supabase Deno Runtime
Yigit Konur
Yigit Konur

Posted on • Edited on

Using NPM Packages on Supabase Deno Runtime

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

  1. Understanding NPM Compatibility
  2. Importing NPM Packages
  3. Managing Dependencies
  4. Working with Node.js Built-ins
  5. Private NPM Packages
  6. TypeScript Support
  7. Local Development Workflow
  8. Deployment
  9. Best Practices
  10. Common Patterns & Examples
  11. 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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

Note the trailing slash for submodules:

{
  "imports": {
    "drizzle-orm": "npm:drizzle-orm@0.29.0",
    "drizzle-orm/": "npm:drizzle-orm@0.29.0/"
  }
}
Enter fullscreen mode Exit fullscreen mode

This allows both:

import { drizzle } from 'drizzle-orm'  // Main module
import { pgTable } from 'drizzle-orm/pg-core'  // Submodule
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }))
})
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

Or use deno.json:

{
  "imports": {
    "express": "npm:express@4.18.2",
    "pg": "npm:pg@8.11.3",
    "process": "node:process"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

Step 3: Set Environment Variable

Local development (.env):

NPM_TOKEN=ghp_your_github_token_here
COMPANY_TOKEN=your_company_token_here
Enter fullscreen mode Exit fullscreen mode

Production:

# Set secrets via CLI
supabase secrets set NPM_TOKEN=ghp_your_token

# Or via Dashboard
# Go to Edge Functions → Secrets → Add Secret
Enter fullscreen mode Exit fullscreen mode

Step 4: Import Private Package

deno.json:

{
  "imports": {
    "my-private-lib": "npm:@myorg/private-package@1.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

index.ts:

import { myFunction } from 'my-private-lib'
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In deno.json

{
  "compilerOptions": {
    "types": ["npm:@types/node"]
  },
  "imports": {
    "express": "npm:express@4.18.2"
  }
}
Enter fullscreen mode Exit fullscreen mode

Local Development Workflow

Step 1: Initialize Supabase

# Install Supabase CLI
npm install supabase@latest --save-dev

# Initialize project
npx supabase init
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Edge Function

npx supabase functions new my-function
Enter fullscreen mode Exit fullscreen mode

This creates:

└── supabase
    └── functions
        └── my-function
            └── index.ts
Enter fullscreen mode Exit fullscreen mode

Step 3: Add Dependencies

Create deno.json:

{
  "imports": {
    "stripe": "npm:stripe@14.0.0",
    "supabase": "npm:@supabase/supabase-js@2"
  }
}
Enter fullscreen mode Exit fullscreen mode

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' } }
  )
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

Deployment

Deploy Single Function

npx supabase functions deploy my-function
Enter fullscreen mode Exit fullscreen mode

Deploy All Functions

npx supabase functions deploy
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Always Pin Versions

Bad:

{
  "imports": {
    "express": "npm:express"  // Unpredictable
  }
}
Enter fullscreen mode Exit fullscreen mode

Good:

{
  "imports": {
    "express": "npm:express@4.18.2"  // Explicit version
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Use deno.json per Function

Recommended structure:

functions/
├── stripe-webhook/
│   ├── index.ts
│   └── deno.json
├── send-email/
│   ├── index.ts
│   └── deno.json
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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 }
    )
  }
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

6. Private Packages Security

# ❌ Don't commit .npmrc with tokens
echo ".npmrc" >> .gitignore

# ✅ Use environment variables in .npmrc
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Supabase Edge Function:

deno.json:

{
  "imports": {
    "express": "npm:express@4.18.2",
    "pg": "npm:pg@8.11.3",
    "process": "node:process"
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Example 2: Stripe Webhook Handler

deno.json:

{
  "imports": {
    "stripe": "npm:stripe@14.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }
    )
  }
})
Enter fullscreen mode Exit fullscreen mode

config.toml:

[functions.stripe-webhook]
verify_jwt = false  # Stripe doesn't send JWT
Enter fullscreen mode Exit fullscreen mode

Example 3: Send Email with Resend

deno.json:

{
  "imports": {
    "resend": "npm:resend@2.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

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' }
  })
})
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

schema.ts:

import { pgTable, serial, text } from 'drizzle-orm/pg-core'

export const notes = pgTable('notes', {
  id: serial('id').primaryKey(),
  title: text('title').notNull()
})
Enter fullscreen mode Exit fullscreen mode

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' }
  })
})
Enter fullscreen mode Exit fullscreen mode

Example 5: OpenAI Integration

deno.json:

{
  "imports": {
    "openai": "npm:openai@4.20.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

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' } }
  )
})
Enter fullscreen mode Exit fullscreen mode

Troubleshooting

Issue 1: "Module not found" Error

Problem:

error: Module not found "https://deno.land/x/..."
Enter fullscreen mode Exit fullscreen mode

Solution:

// ✅ Use npm: prefix for NPM packages
import lodash from 'npm:lodash@4.17.21'

// Not:
// ❌ import lodash from 'lodash'
Enter fullscreen mode Exit fullscreen mode

Issue 2: Process is Not Defined

Problem:

error: process is not defined
Enter fullscreen mode Exit fullscreen mode

Solution:

// ✅ Import Node.js globals
import process from 'node:process'

// Or use Deno API
const secret = Deno.env.get('SECRET')
Enter fullscreen mode Exit fullscreen mode

Issue 3: Submodule Import Fails

Problem:

import { pgTable } from 'drizzle-orm/pg-core'  // Error
Enter fullscreen mode Exit fullscreen mode

Solution:

{
  "imports": {
    "drizzle-orm": "npm:drizzle-orm@0.29.0",
    "drizzle-orm/": "npm:drizzle-orm@0.29.0/"  // Note trailing slash
  }
}
Enter fullscreen mode Exit fullscreen mode

Issue 4: Private Package 401 Unauthorized

Problem:

error: npm package '@myorg/private-pkg' could not be downloaded. 401 Unauthorized
Enter fullscreen mode Exit fullscreen mode

Solution:

  1. Check .npmrc syntax
  2. Verify token is set: echo $NPM_TOKEN
  3. Test token: npm whoami --registry=https://your-registry
  4. Ensure token in secrets: supabase secrets list

Issue 5: Function Timeout

Problem:
Large dependencies causing cold start timeout.

Solution:

  1. Minimize dependencies
  2. Use smaller alternatives
  3. Consider lazy loading:
Deno.serve(async (req) => {
  // Only import when needed
  const { default: heavyLib } = await import('npm:heavy-library@1.0.0')

  // Use library
})
Enter fullscreen mode Exit fullscreen mode

Issue 6: Type Errors

Problem:

error: Type 'any' is not assignable to type 'string'
Enter fullscreen mode Exit fullscreen mode

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"]
  }
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Summary

You now have a comprehensive understanding of using NPM packages on Supabase's Deno runtime:

  1. Import directly using npm: specifier with versions
  2. Manage dependencies with deno.json per function
  3. Use Node.js APIs with node: specifier
  4. Work with private packages via .npmrc
  5. Get TypeScript support automatically or via @deno-types
  6. Develop locally with supabase functions serve
  7. 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)