Packages & Modules

GasPackᵐ organizes code into packages that contain one or more modules. Each module is a self-contained unit with its own API, exported as a global namespace in Google Apps Script.

@company.com/utilities          ← package
├── logger    → LOGGER_V1       ← module → namespace
├── storage   → STORAGE_V1      ← module → namespace
└── crypto    → CRYPTO_V1       ← module → namespace
ConceptWhat it isExample
PackageContainer for related modules. Versioned with semver. Published as a unit.@company.com/utilities
ModuleIndividual component with its own version and API. Exported as a namespace.logger
NamespaceThe global variable name in Apps Script. Includes a version suffix derived from the module's major version.LOGGER_V1

Why namespaces?

Google Apps Script has no import system. All code shares a single global scope. GasPackᵐ modules are compiled into IIFE-wrapped namespace globals so they don't collide:

function myScript() {
  LOGGER_V1.info("Starting...");
  const data = STORAGE_V1.get("key");
  const hash = CRYPTO_V1.hash(data);
  return hash;
}

Versioning Model

GasPackᵐ has three levels of versioning. Understanding them is essential because in Apps Script, namespace references are hardcoded in your code — there's no abstraction layer to hide version changes.

The three levels

LevelWhat it controlsExample
Module versionIndividual module's semver. The major number drives the namespace suffix.logger: 1.5.2LOGGER_V1
logger: 2.5.2LOGGER_V2
Namespace versionDerived from the module's major version. This is what you type in your Apps Script code._V1, _V2
Package versionAdditive rollup of all module changes. Each module bump adds to the package version: patch → +0.0.1, minor → +0.1.0 (patch resets), major → +1.0.0 (minor & patch reset). When multiple modules are bumped at once, the highest-impact type is applied.@company.com/utilities@2.3.1

What changes require what bumps

Module changeNamespaceYour code
1.2.3 → 1.2.4 (patch)_V1 (same)No changes needed
1.2.3 → 1.3.0 (minor)_V1 (same)No changes needed
1.2.3 → 2.0.0 (major)_V1_V2Find & replace all references

Example: package evolution

v1.0.0 - Initial release
  LOGGER 1.0.0 (LOGGER_V1)
  STORAGE 1.0.0 (STORAGE_V1)

v1.1.0 - Logger enhancement
  LOGGER 1.1.0 (LOGGER_V1)      ← added log levels, namespace unchanged
  STORAGE 1.0.0 (STORAGE_V1)

v2.0.0 - Storage breaking change
  LOGGER 1.1.0 (LOGGER_V1)      ← unchanged
  STORAGE 2.0.0 (STORAGE_V2)    ← API redesigned, namespace changed

When the package bumps to v2.0.0, only STORAGE has a namespace change. Any Apps Script code that references STORAGE_V1 needs to find-and-replace to STORAGE_V2. All LOGGER_V1 calls remain unchanged — no action required.

For package maintainers whose modules depend on STORAGE, adopting V2 is entirely optional. Their existing code continues to work on the V1 maintenance line. If a maintainer wants to take advantage of the new V2 API, they would update their dependency declaration, refactor their source code to use STORAGE_V2, and publish a new version of their own package. See Module Dependencies below for how to declare and manage these cross-package relationships.

Bump CLI commands

All versioning targets modules — the package version is an additive rollup of module bumps over time. When bumping multiple modules at once, the highest-impact type determines the package bump.

# Bump a specific module
gpm bump logger patch    # 1.5.2 → 1.5.3 (LOGGER_V1 stays)
gpm bump logger minor    # 1.5.2 → 1.6.0 (LOGGER_V1 stays)
gpm bump logger major    # 1.5.2 → 2.0.0 (LOGGER_V1 → LOGGER_V2)

# Bump multiple modules at once
gpm bump logger patch storage minor

# Show current module versions
gpm bump

Module-Level Dependencies

Modules can declare exactly which modules from other packages they depend on. This powers selective installation — when a developer installs your package, only the needed dependency modules are pulled in, not entire packages.

How it works

Imagine two packages in the registry, each with several modules:

@company.com/logger                    @company.com/storage
├── console   (LOGGER_CONSOLE_V1)      ├── script   (STORAGE_SCRIPT_V1)
├── sheet     (LOGGER_SHEET_V1)        ├── user     (STORAGE_USER_V1)
└── formatter (LOGGER_FORMATTER_V1)    ├── cache    (STORAGE_CACHE_V1)
                                       └── memory   (STORAGE_MEMORY_V1)

Now you're building a package called @you.gmail.com/data-tools with a sync module. Your sync code logs results to a spreadsheet using LOGGER_SHEET_V1 and saves state using STORAGE_SCRIPT_V1. It doesn't use console logging, the formatter, user storage, cache, or memory storage.

Step 1: Declare what your module depends on

gpm dep add sync --dep @company.com/logger/sheet
gpm dep add sync --dep @company.com/storage/script

This adds the dependencies to your gpm.json:

{
  "modules": {
    "sync": {
      "namespace": "DATA_SYNC",
      "entryPoint": "src/sync/index.js",
      "dependencies": [
        "@company.com/logger/sheet",
        "@company.com/storage/script"
      ]
    }
  }
}

Step 2: Build and publish

gpm build --strict-deps    # validates deps match your code
gpm publish --access public

The dependency declarations are included in the published package metadata and stored in the registry.

Step 3: A developer installs your package

gpm install @you.gmail.com/data-tools

The registry sees that sync depends on logger/sheet and storage/script. It installs only those two modules — not all 3 logger modules and not all 4 storage modules. The developer's project stays lean.

CLI commands

# Add a dependency by canonical ref (@scope/package/module)
gpm dep add sync --dep @company.com/logger/sheet

# Add by namespace (resolved via registry lookup)
gpm dep add sync --dep-ns LOGGER_SHEET_V1

# List dependencies for a module
gpm dep list sync

# Scan compiled code and detect missing deps
gpm dep detect sync

# Remove a dependency
gpm dep remove sync --dep @company.com/logger/sheet

Build validation

Use gpm build --strict-deps to turn dependency warnings into errors. The build scans compiled code for namespace references and cross-checks against declared dependencies.

Domain Scoping

Every package is scoped to a domain: @domain/package-name. This prevents naming conflicts and ties packages to verified identities.

Personal domains

Every user automatically gets a personal domain derived from their Google account email. Available immediately on login.

@jane.doe.gmail.com/my-utils
@joe.doe.company.com/data-tools

Workspace domains Coming Soon

Teams and Enterprise accounts can register their Google Workspace domain as a package scope by adding a verification code as a DNS TXT entry. This enables team-wide publishing under a shared domain.

@acme-corp.com/billing-utils
@velocitydog.com/internal-tools

Workspace domains unlock organizational capabilities:

  • Teams — publish public and private packages under the shared workspace domain
  • Enterprise — all Teams features plus self-hosted private registry for on-premise hosting
Personal DomainWorkspace Domain Coming Soon
SourceGoogle account emailGoogle Workspace + DNS verification
SetupAutomatic on loginAdmin registers domain via DNS TXT record
Who can publishAccount owner onlyAuthorized workspace members
Public packagesYesYes
Private packagesPro accountYes (Teams & Enterprise)
Self-hosted registryNoEnterprise only

Managing Major Version Changes

As a package maintainer, you have a real impact on the developers who depend on your work. A module major bump changes the namespace — STORAGE_V1 becomes STORAGE_V2 — and every project and dependent package using that namespace needs to update. With thoughtful API design, you can make these changes rare and well-managed.

Design for longevity

The best major bump is the one you never have to make. These patterns help you evolve your API without breaking existing code:

PatternWhy it works
Add new functions alongside existing onesExisting callers are unaffected — this is a minor bump
Use optional parameters to extend signaturesAdding a trailing optional parameter never breaks existing calls
Accept option objects instead of positional argsNew properties can always be added safely
Deprecate before removing with a logged warningGives developers time to migrate before the function disappears

When a major change is the right call

Sometimes a breaking change is necessary — a fundamental redesign, a security fix that changes return types, or removing an API that was a mistake. When this happens, the best practice is to maintain both version lines:

@company.com/utilities
├── v1.x (STORAGE_V1) ← maintenance branch: security patches, critical fixes
└── v2.x (STORAGE_V2) ← active branch: new features land here

Both remain available in the registry. Projects and packages on V1 continue receiving patches without being forced to refactor. Developers can migrate to V2 on their own timeline.

Keep a maintenance branch

After publishing a major bump, keep a Git branch for the previous major version (e.g., v1-maintenance). Apply security patches and critical bug fixes to both branches, and publish patch releases for each. This ensures developers who haven't migrated yet still receive important fixes.

Why this matters

In Apps Script, namespace references are hardcoded globals — there's no import system to abstract them away. A project may depend on several packages that all use STORAGE_V1. Upgrading to _V2 requires every one of those package maintainers to release compatible versions first. By maintaining both version lines and designing for backward compatibility, you make the ecosystem more resilient for everyone.