Skip to content

functions deploy fails to read .npmrc environment variables for private npm registry auth (workspace deno.json) #4927

@addniner

Description

@addniner

Bug report

Describe the bug

When using a root-level workspace deno.json at supabase/functions/deno.json (instead of per-function deno.json files), supabase functions deploy fails to resolve the environment variable in .npmrc.

The .npmrc file uses ${NPM_AUTH_TOKEN} for private registry authentication. functions serve reads this variable correctly (from supabase/functions/.env), but functions deploy does not — resulting in a registry authentication failure.

.npmrc configuration

@myorg:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_AUTH_TOKEN}

The NPM_AUTH_TOKEN is provided via supabase/functions/.env, which functions serve loads automatically.

Project structure

supabase/functions/
├── deno.json              # Root workspace config
│   ├── "workspace": ["./*"]
│   └── "imports": {
│         "@my-private-pkg": "npm:@myorg/my-package@^1.0.0",
│         ...
│       }
├── .npmrc                 # Private registry auth
├── .env                   # NPM_AUTH_TOKEN=ghp_xxxxx
├── function-a/
│   ├── index.ts
│   └── deno.json          # Function-specific imports only
├── function-b/
│   └── ...
└── _shared/               # Shared utilities

Why this structure?
The official docs recommend per-function deno.json, but this creates a dependency management problem with _shared/ code — shared utilities would need their npm dependencies duplicated across every function's deno.json. A root workspace deno.json solves this. This approach is also discussed as practical in Discussion #33595.

Steps to reproduce

  1. Set up the project structure above with a private npm package
  2. Configure .npmrc with ${NPM_AUTH_TOKEN} environment variable
  3. Set the token in supabase/functions/.env
  4. Run supabase functions serveWorks, private package resolves correctly
  5. Run supabase functions deployFails, authentication error (.npmrc env var not resolved)

Expected behavior

functions deploy should resolve environment variables in .npmrc the same way functions serve does.

Workaround

Running functions serve before deploy makes it work. The serve process appears to cache the resolved npm packages, which deploy then picks up:

# In CI/CD pipeline:

- name: Start Supabase services
  run: supabase start -x imgproxy,supavisor,realtime,storage-api,studio,mailpit,logflare,vector

- name: Warm up functions (workaround)
  run: |
    supabase functions serve --no-verify-jwt &
    SERVE_PID=$!
    sleep 3
    # Must call a function that imports the private package
    curl -i --fail http://127.0.0.1:54321/functions/v1/health || true
    kill $SERVE_PID || true
    wait $SERVE_PID 2>/dev/null || true

- name: Deploy Edge Functions
  run: supabase functions deploy --no-verify-jwt --prune --yes

Important: The health endpoint must actually import the private package — simply running serve is not enough. The package needs to be loaded at least once.

// health/index.ts
import {} from "@my-private-pkg"; // Force package resolution

Deno.serve(() => new Response("ok", { status: 200 }));

This was discovered empirically after multiple failed attempts and has been the only reliable CI/CD method.

Environment

  • Supabase CLI: v2.75.0
  • Deno: v2.7.1
  • OS: Ubuntu 24.04 (GitHub Actions) / macOS (local)

Related issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions