npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@scopmx/surveybase-server

v1.3.0

Published

Survey backend. Mount it on the host Express app and the host MongoDB.

Readme

@scopmx/surveybase-server

Express router and MongoDB models for Surveybase. Mount it in your own Express app, next to your own MongoDB connection. It serves:

  • the admin API used by <SurveyAdmin />;
  • the public API used by <SurveyWidget /> on your website.

Frontend package: @scopmx/surveybase-widget.

Options marked (next release) are in the source but not in the published 1.1.0 yet.


Contents

  1. Install
  2. Quick start
  3. surveyApi(options)
  4. mountSurveyApi(app, options)
  5. All options
  6. Environment variables
  7. Keys and admin access
  8. Live results
  9. Rate limits
  10. CORS
  11. Endpoints
  12. Database
  13. Production checklist

Install

npm install @scopmx/surveybase-server express mongoose

| Requirement | Version | | ----------------- | ------- | | Node.js | >= 18 | | express (peer) | ^4.21 | | mongoose (peer) | ^8 |


Quick start

const express = require("express");
const mongoose = require("mongoose");
const { surveyApi } = require("@scopmx/surveybase-server");

const app = express();
mongoose.connect(process.env.MONGODB_URI);

app.use(
  "/api/survey",
  surveyApi({
    authorize: (req) => isAdminSession(req), // your own login check
    defaultTheme: { primaryColor: "#0D447A" }
  })
);

app.listen(4000);

surveyApi(options)

Returns an Express Router. Mount it at any path, usually /api/survey (the frontend default).

  • It works before MongoDB is connected. Requests wait up to 15 seconds for the connection, then answer 503.
  • Setup (models, install keys, live results) runs once, on the first connection.
  • If setup fails, the next request retries it.

mountSurveyApi(app, options)

Mounts the router on app at options.basePath and finishes setup before it resolves. Use it when you want the keys at startup.

await mongoose.connect(process.env.MONGODB_URI);

const { basePath, siteKey, secretKey, workspaceId } = await mountSurveyApi(app, {
  connection: mongoose.connection,
  basePath: "/api/survey"
});
  • connection is required, and it must already be connected. Otherwise it throws.
  • It resolves { basePath, secretKey, siteKey, workspaceId }. secretKey is empty unless it was generated during this start.

Other exports: DEFAULT_BASE_PATH ("/api/survey"), and the TypeScript types SurveyApiOptions, SurveyInstallKeys, LiveResultsAdapter and LiveResultsMessage.


All options

Every option is optional, except connection for mountSurveyApi.

Connection and routing

| Option | Type | Default | Description | | ------------ | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | connection | Mongoose Connection | mongoose.connection | Connection the survey models use. Required for mountSurveyApi. | | basePath | string | "/api/survey" | URL prefix. Used by mountSurveyApi only; with surveyApi you choose the path in app.use. | | cors | false \| object | { origin: true } | CORS for the public widget routes (/public, /health). Takes cors options. false skips CORS. | | adminCors | false \| object | see CORS | CORS for the admin routes (/auth, /projects, /workspace). |

Access and keys

| Option | Type | Default | Description | | ------------------- | -------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | authorize | (req) => boolean \| Promise<boolean> | none | Lets admin requests in without an sk_ key, for example by checking your app's session or a short-lived token. Returning true grants full admin access. | | secretKey | string | SURVEY_SECRET_KEY | Admin key: sk_ plus at least 32 random characters. Only a SHA-256 hash is stored. | | previousSecretKey | string | SURVEY_PREVIOUS_SECRET_KEY | Old key that keeps working during a key rotation. | | siteKey | string | SURVEY_SITE_KEY, the stored key, or generated | Public pk_ key used by the widget. | | logKeys | boolean | false | Print the site key and a masked secret key on startup. |

Install

| Option | Type | Default | Description | | -------------------------- | ------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | workspaceId | string | the only workspace | Pin the install to one workspace id. | | workspaceName | string | SURVEY_WORKSPACE_NAME or "Workspace" | Used only when the install creates the workspace. | | admin | { name?, email? } | SURVEY_ADMIN_NAME / SURVEY_ADMIN_EMAIL, or "Admin" / "[email protected]" | First admin user, used only when the install creates the workspace. | | migrateLegacyCollections | boolean | false | Renames old unprefixed collections (users, workspaces, members, projects, responses) to survey_*. Never enable it on a database where your app owns a users or projects collection. An existing survey_* target is kept as survey_*_backup_<time>. |

Surveys

| Option | Type | Default | Description | | -------------- | --------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | defaultTheme | { primaryColor?: string } | { primaryColor: "#3d2ec5" } | (next release) Colour for new surveys: blank ones, ones from a template, and duplicates whose source has no colour. Existing surveys keep their colour. primaryColor must be a 6-digit hex (#0D447A); anything else throws at startup. A colour sent by the admin (defaultSurveyColor) wins. |

Limits and live results

| Option | Type | Default | Description | | ---------------- | ------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------ | | rateLimit | false \| object | see Rate limits | Per-IP limits for the public routes. false turns them off. | | liveResults | "auto" \| "change-streams" \| "poll" \| "memory" \| adapter | "auto" | How new responses reach open admin pages. See Live results. | | livePollMs | number | 15000 | Poll interval sent to admins when live results fall back to polling. | | maxLiveStreams | number | 20 | Live result connections allowed open at once, per workspace, on this instance. |


Environment variables

Options always win over environment variables.

| Variable | Same as option | | ---------------------------- | ------------------- | | SURVEY_SECRET_KEY | secretKey | | SURVEY_PREVIOUS_SECRET_KEY | previousSecretKey | | SURVEY_SITE_KEY | siteKey | | SURVEY_WORKSPACE_NAME | workspaceName | | SURVEY_ADMIN_NAME | admin.name | | SURVEY_ADMIN_EMAIL | admin.email |

With NODE_ENV=production, install errors stop the server instead of only being logged.


Keys and admin access

There is a single admin: whoever passes the check below can manage every survey.

An admin request is accepted when either:

  1. it sends a valid secret key as x-survey-key: sk_... or Authorization: Bearer sk_... (the current or the previous key); or
  2. it does not send an sk_ key and your authorize(req) returns true.

Otherwise the server answers 401. A suspended workspace answers 403.

Recommended setup: use authorize with your own login, and on the frontend send your token with getToken. Never put the sk_ key in browser code.

surveyApi({
  authorize: async (req) => {
    const token = (req.get("authorization") || "").replace(/^Bearer\s+/i, "");
    const user = await verifyJwt(token).catch(() => null);
    return Boolean(user?.isAdmin);
  }
});

Key behaviour:

  • Without secretKey, a key is generated on the first start and printed once. Save it.
  • To rotate, set the new key as secretKey and the old one as previousSecretKey, deploy, then remove previousSecretKey.
  • The site key (pk_) is public. It only lets the widget read live surveys and submit answers.

Live results

Open admin results pages receive new responses as they arrive (Server-Sent Events on /projects/:id/responses/stream). Choose how instances share them:

| Value | Behaviour | | ------------------ | ------------------------------------------------------------------------ | | "auto" (default) | MongoDB change streams on a replica set, otherwise polling. | | "change-streams" | Same as "auto", but logs an error when change streams are unavailable. | | "poll" | Admins refresh every livePollMs. | | "memory" | In-process only. Correct for one instance. | | adapter object | Your own pub/sub, for example Redis. |

Adapter interface:

interface LiveResultsAdapter {
  publish(message: { projectId: string; response: object }): void | Promise<void>;
  // Must also receive messages published by this same process.
  subscribe(handler: (message) => void): void | (() => void);
}

The frontend can turn live updates off completely with results={{ live: false }}. The admin then only loads data on open, on filter changes and on Refresh.


Rate limits

Fixed one-minute windows, counted per IP address, in memory, per instance. They apply to the public routes only.

| Key | Default | Applies to | | -------------- | ------- | -------------------------------------------------------- | | windowMs | 60000 | Window length in ms | | list | 120 | GET /public/projects | | views | 60 | POST /public/projects/:publicKey/views, per survey | | responses | 60 | POST /public/projects/:publicKey/responses, per survey | | newResponses | 10 | New responses created, per survey |

surveyApi({ rateLimit: { list: 300, newResponses: 20 } }); // override some
surveyApi({ rateLimit: false }); // turn off
  • Over the limit, the server answers 429 with a Retry-After header.
  • Behind a proxy or load balancer, set app.set("trust proxy", ...) so req.ip is the visitor's address.
  • With several instances, also put a shared limiter in front.

CORS

| Routes | Option | Default | | ----------------------------------------- | ----------- | --------------------------------------------------------------------------- | | Public: /public/*, /health | cors | { origin: true } (any site can embed the widget) | | Admin: /auth, /projects, /workspace | adminCors | cors when it lists specific origins, otherwise false (same origin only) |

surveyApi({
  cors: { origin: ["https://www.example.com"] },
  adminCors: { origin: ["https://admin.example.com"], credentials: true }
});

credentials: true with an open origin (true, "*" or none) throws at startup.


Endpoints

All paths are relative to the mount path (default /api/survey). Bodies are JSON (limit 1 MB). Errors are { "error": "..." } with a matching status code.

Public (widget)

Each route also exists under /public/sites/:siteKey/..., for one API serving several sites.

| Method | Path | Description | | ------ | --------------------------------------- | ------------------------------------------------------------ | | GET | /health | Health check. | | GET | /public/projects | Live surveys for the visitor. Query: traits, respondent. | | POST | /public/projects/:publicKey/views | Count a view. | | POST | /public/projects/:publicKey/responses | Save or update an answer set. |

Admin (needs admin access)

| Method | Path | Description | | -------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | GET | /auth/me | Current admin and workspace. | | GET | /workspace | Workspace settings, including the site key. | | PATCH | /workspace | Update workspace settings. | | GET | /projects | List surveys. Query: status = draft, live or archived. | | POST | /projects | Create a survey. Body: { name, theme?: { primaryColor } }. Without a colour it uses defaultTheme; an invalid hex returns 400. | | GET | /projects/:id | One survey with questions. | | PATCH | /projects/:id | Update name, status, theme, targeting, thank-you screen, and so on. | | DELETE | /projects/:id | Delete a survey and its responses. | | POST | /projects/:id/duplicate | Copy a survey, keeping its colour. | | POST | /projects/:id/questions | Add a question. | | PUT | /projects/:id/questions/reorder | Reorder questions. | | PATCH | /projects/:id/questions/:questionId | Update a question. | | DELETE | /projects/:id/questions/:questionId | Delete a question. | | GET | /projects/:id/responses | Responses for the summary (latest 1,000 by default, limit up to 2,000). | | GET | /projects/:id/responses/page | (next release) Paged list, newest first: { responses, nextCursor }. limit is 30 by default, up to 100; pass cursor to get the next page. | | GET | /projects/:id/responses/answers | Text answers for one question: questionId, q, limit (8 by default, up to 30), cursor. | | GET | /projects/:id/responses/contacts/:questionId | (next release) Contact form table: page, limit (10, 25, 50 or 100), sort = newest or oldest, q. | | GET | /projects/:id/responses/contacts/:questionId/emails | (next release) Unique emails (up to 5,000, with truncated). countOnly=1 returns only the count. | | GET | /projects/:id/responses/stream | Live results (Server-Sent Events). |

Shared response filters (query string) for the responses, page, answers and contacts endpoints:

| Param | Values | | ----------------------- | ---------------------------------------------------------------------- | | from, to | ISO dates. from is inclusive, to is exclusive. | | compareFrom | ISO date. Start of the previous period, for comparison (summary only). | | device | desktop or mobile | | status | completed or partial | | search | Text search across answers and visitor traits | | questionId + answer | Responses whose answer to that question matches |


Database

  • Collections are prefixed survey_ (survey_users, survey_workspaces, survey_projects, survey_responses, plus survey_install_lock used during the first start), so they live safely next to your own collections.
  • Indexes are created by Mongoose on startup. Response paging uses { projectId: 1, createdAt: -1, _id: -1 }.
  • Use a replica set (for example MongoDB Atlas) if you want liveResults: "auto" to use change streams.

Production checklist

  • Set secretKey (or SURVEY_SECRET_KEY), or use authorize with your own login. Keep sk_ keys out of browser code.
  • Set app.set("trust proxy", ...) behind a proxy, so rate limits see real visitor IPs.
  • Restrict cors to your site origins if the widget only runs on known domains.
  • Running several instances: use liveResults: "auto" on a replica set, "poll", or an adapter. Do not use "memory".
  • Leave migrateLegacyCollections off unless you are upgrading an old standalone install.
  • Optional: set defaultTheme so new surveys match your brand.