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

hydrousdb

v3.5.6

Published

Official JavaScript/TypeScript SDK for HydrousDB — the backend-as-a-service platform for records, auth, storage, and analytics.

Readme

HydrousDB JS/TS SDK

A database that doesn't choke on big JSON. Store, query, and analyse massive records — with auth, Google sign-in, and file storage built in.

npm install hydrousdb

Get your free API keys at hydrousdb.com


Table of Contents


Setup

import { createClient } from 'hydrousdb';

const db = createClient({
  authKey:           process.env.HYDROUS_AUTH_KEY!,          // hk_auth_…
  bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,        // hk_bucket_…
  storageKeys: {
    main: process.env.HYDROUS_STORAGE_KEY!,                  // ssk_…
    // add more named storage keys as needed
  },
  // baseUrl: 'https://custom-endpoint.example.com',         // optional override
});

You get three key types from the dashboard — one for auth, one for records/analytics, one (or more) for file storage. Keep them in environment variables, never in your code.

Works in React, Next.js, Vue, React Native, Node.js — anywhere that runs modern JavaScript.


Records

JSON objects stored in named buckets. Every record automatically gets id, createdAt, and updatedAt.

const posts = db.records('blog-posts');

Create, Read, Update, Delete

// Create
const post = await posts.create(
  { title: 'Hello', status: 'draft', views: 0 },
  {
    queryableFields: ['status', 'views'],  // declare fields you want to filter on
    userEmail: '[email protected]',        // optional — for audit trails
  },
);
console.log(post.id);        // "260601-rec_01JA2XYZ"
console.log(post.createdAt); // Unix ms timestamp

// Read by ID
const found = await posts.get(post.id);

// Partial update — only the fields you pass are changed
await posts.patch(post.id, { status: 'published' });

// Merge mode — deeply merge nested objects instead of replacing them
await posts.patch(post.id, { meta: { seo: true } }, { merge: true });

// Delete permanently
await posts.delete(post.id);

Why queryableFields? Records are stored as compressed blobs. Fields you want to filter or sort by must be declared at write time. You only pay the indexing overhead for what you actually query.


Querying & Filtering

const { records, hasMore, nextCursor } = await posts.query({
  filters: [
    { field: 'status', op: '==',       value: 'published' },
    { field: 'views',  op: '>',        value: 100 },
    { field: 'title',  op: 'contains', value: 'hello' },
  ],
  orderBy: 'createdAt',
  order:   'desc',
  limit:   20,
  fields:  'id,title,status', // optional — return only these fields
});

Supported filter operators: == != > < >= <= contains

You can combine multiple filters. The first equality filter (==) drives the GCS index — additional filters are applied in-memory after hydration. This means you get fast indexed lookups for equality filters and flexible in-memory filtering for everything else.

Filter combinations:

// Equality only — fast index path
await posts.query({ filters: [{ field: 'status', op: '==', value: 'published' }] });

// Equality + range — index on status, range applied in-memory
await posts.query({ filters: [
  { field: 'status', op: '==', value: 'published' },
  { field: 'views',  op: '>=', value: 100 },
]});

// Equality + contains — index on status, contains applied in-memory
await posts.query({ filters: [
  { field: 'status', op: '==',       value: 'published' },
  { field: 'title',  op: 'contains', value: 'hello' },
]});

// Multiple equality — first drives index, rest applied in-memory
await posts.query({ filters: [
  { field: 'status',   op: '==', value: 'published' },
  { field: 'category', op: '==', value: 'tech' },
]});

Time Scope on Queries

Pass timeScope to restrict records to a specific day, month, or year. This is the fastest way to scope a query by time — no timestamp arithmetic needed.

| Scope | Format | Example | Matches | |---|---|---|---| | Day | _day_YYMMDD | _day_260305 | March 5, 2026 | | Month | _month_YYMM | _month_2603 | March 2026 | | Year | _year_YY | _year_26 | All of 2026 |

// All records from March 2026
const { records, hasMore, nextCursor } = await posts.query({
  timeScope: '_month_2603',
  order:     'desc',
  limit:     50,
});

// Paginate through a time-scoped result set
if (hasMore) {
  const page2 = await posts.query({
    timeScope:  '_month_2603',
    startAfter: nextCursor,
  });
}

// A specific day
const { records: dayRecords } = await posts.query({
  timeScope: '_day_260305',   // March 5, 2026
  order:     'asc',
});

// An entire year
const { records: yearRecords } = await posts.query({
  timeScope: '_year_26',
  orderBy:   'createdAt',
  order:     'asc',
  limit:     100,
});

// Fully composable with filters — this is the recommended pattern for large buckets
const { records: published } = await posts.query({
  timeScope: '_month_2603',
  filters:   [{ field: 'status', op: '==', value: 'published' }],
  orderBy:   'createdAt',
  order:     'desc',
});

// getAll() also respects timeScope
const all = await posts.getAll({ timeScope: '_year_26' });

Date Range Queries

Pass startDate and/or endDate (ISO date strings) to walk records within a calendar range. Both fields are optional — omit either for an open-ended range.

// Records from a specific month
const { records } = await posts.query({
  startDate: '2026-04-01',
  endDate:   '2026-04-30',
});

// Records since a specific date (no end bound)
const { records: recent } = await posts.query({
  startDate: '2026-01-01',
  orderBy:   'createdAt',
  order:     'desc',
});

// Walk a specific year using the year shorthand
const { records: yearRecords } = await posts.query({
  year:  '26',        // two-digit year
  order: 'desc',
  limit: 100,
});

// Combine with filters
const { records: paidOrders } = await orders.query({
  startDate: '2026-04-01',
  endDate:   '2026-04-30',
  filters:   [{ field: 'status', op: '==', value: 'paid' }],
});

// Sort by any field
const { records: byPrice } = await rooms.query({
  timeScope: '_month_2604',
  filters:   [{ field: 'status', op: '==', value: 'available' }],
  sortBy:    'pricePerNight',
  order:     'asc',
});

Pagination

query() returns a cursor you can pass straight into the next call.

// Page 1
const page1 = await posts.query({ limit: 20, orderBy: 'createdAt', order: 'desc' });

// Page 2
if (page1.hasMore) {
  const page2 = await posts.query({
    limit:      20,
    orderBy:    'createdAt',
    order:      'desc',
    startAfter: page1.nextCursor,   // cursor-based — no offset drift
  });
}

// You can also use startAt / endAt for range-based cursor control
const window = await posts.query({ startAt: cursorA, endAt: cursorB });

Atomic Field Updates

Avoid race conditions with server-side sentinels inside patch():

await posts.patch(post.id, {
  views:    { __op: 'increment',       delta: 1 },          // add N
  credits:  { __op: 'decrement',       delta: 5 },          // subtract N
  slug:     { __op: 'setOnce',         value: 'my-post' },  // set only if currently empty
  tags:     { __op: 'appendUnique',    item: 'featured' },  // add to array, no duplicates
  oldTag:   { __op: 'removeFromArray', item: 'draft' },     // remove from array
  rating:   { __op: 'clamp',           value: 6, min: 0, max: 5 }, // clamp to range
  price:    { __op: 'multiplyBy',      factor: 1.1 },       // multiply
  active:   { __op: 'toggleBool' },                         // flip boolean
  syncedAt: { __op: 'serverTimestamp' },                    // set to server time
  score:    { __op: 'setIf', value: 99, cond: { op: '>=', value: 50 } }, // conditional set
} as any);

Enable audit trails and history with extra patch options:

await posts.patch(
  post.id,
  { status: 'published' },
  { userEmail: '[email protected]', trackHistory: true },
);

Batch Operations

// Create up to 500 records at once
const { results, errors, successful, failed } = await posts.batchCreate(
  [{ title: 'A', status: 'draft' }, { title: 'B', status: 'draft' }],
  { queryableFields: ['title', 'status'], userEmail: '[email protected]' },
);

// Update up to 500 records at once
const { successful, failed } = await posts.batchUpdate(
  [
    { recordId: 'id1', values: { status: 'archived' } },
    { recordId: 'id2', values: { status: 'archived' } },
  ],
  '[email protected]', // optional userEmail
);

// Delete up to 500 records at once
const { successful, failed } = await posts.batchDelete(['id1', 'id2']);

Batch upsert using custom IDs — include _customRecordId on each item:

await posts.batchCreate([
  { _customRecordId: '260601-post_hello', title: 'Hello' },
  { _customRecordId: '260601-post_world', title: 'World' },
] as any);

Version History

Every write is automatically versioned when trackHistory is enabled.

// List all saved versions
const history = await posts.getHistory(post.id);
// → [{ generation, savedAt, savedBy, sizeBytes }, …]

// Retrieve a specific past version
const v1 = await posts.getVersion(post.id, history[0].generation!);

Custom Record IDs

Supply your own ID at creation time — if it already exists the record is upserted in-place.

// Format: YYMMDD-segment1__segment2
const post = await posts.create(
  { title: 'Welcome' },
  { customRecordId: '260601-post_welcome' },
);

Existence Check

A lightweight HEAD request — much cheaper than fetching the full record:

const exists = await posts.exists(post.id); // true | false

Get All Records

Fetches every record matching the options without filter support. Use query() when you need filters.

const all = await posts.getAll({
  orderBy: 'createdAt',
  order:   'desc',
  limit:   500,
});

Auth

A complete user system — signup, login, Google sign-in, sessions, password reset, email verification, and admin controls.

const auth = db.auth();

Sign Up & Log In

// Sign up — extra fields beyond email/password are stored on the user
const { user, session } = await auth.signup({
  email:    '[email protected]',
  password: 'hunter2',
  fullName: 'Alice Smith',
  plan:     'pro',               // any custom fields you want
});

// Log in
const { user, session } = await auth.login({
  email:    '[email protected]',
  password: 'hunter2',
});

// Log out this device
await auth.logout({ sessionId: session.sessionId });

// Log out everywhere
await auth.logout({ sessionId: session.sessionId, allDevices: true });

Store session.sessionId and session.refreshToken in your app.

| Token | Lifetime | |---|---| | sessionId | 24 hours | | refreshToken | 30 days |


Google Sign-In

Allow users to sign in or create an account with one tap — no password required.

The flow is the same regardless of platform: get a Google ID token on the client, pass it to continueWithGoogle. The server verifies the token and returns a session identical in shape to login() and signup().

Web (Google Identity Services)

<!-- Add to your HTML <head> -->
<script src="https://accounts.google.com/gsi/client" async defer></script>
google.accounts.id.initialize({
  client_id: 'YOUR_GOOGLE_CLIENT_ID',  // from Google Cloud Console
  callback: async ({ credential }) => {
    const { user, session, isNew } = await db.auth().continueWithGoogle({
      idToken: credential,
    });
    if (isNew) router.push('/onboarding');  // brand-new account
    else       router.push('/dashboard');   // returning user
  },
});

// Render the button anywhere in your page
google.accounts.id.renderButton(
  document.getElementById('google-btn'),
  { theme: 'outline', size: 'large', text: 'continue_with' },
);

React Native

import { GoogleSignin } from '@react-native-google-signin/google-signin';

GoogleSignin.configure({ webClientId: 'YOUR_GOOGLE_CLIENT_ID' });

const { idToken } = await GoogleSignin.signIn();
const { user, session, isNew } = await db.auth().continueWithGoogle({ idToken });
if (isNew) navigation.navigate('Onboarding');

Flutter

final googleUser = await GoogleSignIn().signIn();
final auth       = await googleUser!.authentication;
// Send auth.idToken to your backend which calls the HydrousDB SDK

What isNew tells you

const { user, session, isNew } = await auth.continueWithGoogle({ idToken });

if (isNew) {
  // Account was just created — show onboarding, collect extra info, etc.
  console.log('Welcome!', user.fullName);
} else {
  // Returning user — go straight to the app
  console.log('Welcome back!', user.email);
}

Link Google to an existing email/password account

// User is signed in with email. They click "Connect Google" in settings.
const { idToken } = await GoogleSignin.signIn();
const updatedUser = await auth.linkGoogle({
  sessionId: currentSession.sessionId,
  idToken,
});
// After linking, the user can sign in with either method

Unlink Google

// Only works if the user has a password set — prevents account lockout
await auth.unlinkGoogle({ sessionId: currentSession.sessionId });

Setting a password on a Google-only account

// Google users have no password by default.
// Pass an empty string for currentPassword to set the first one.
await auth.changePassword({
  sessionId:       session.sessionId,
  userId:          user.id,
  currentPassword: '',         // empty — no password set yet
  newPassword:     'newpassword123',
});
// After this, the user can sign in with email+password AND Google

The UserRecord for Google users includes:

interface UserRecord {
  authProvider?: 'email' | 'google';  // 'google' for Google sign-in users
  picture?:      string | null;        // profile photo URL from Google
  googleId?:     string;               // stable Google identifier
  // ... all other standard fields
}

Session Management

// Validate a session and get the current user (use on your backend)
const { user, session } = await auth.validateSession(session.sessionId);
// session → { sessionId, expiresAt }

// Get a brand-new session from a refresh token (before the old one expires)
const newSession = await auth.refreshSession(session.refreshToken);

User Profile

// Fetch a user by ID
const user = await auth.getUser(session.userId);

// Update profile fields (users can update themselves; admins can update anyone)
await auth.updateUser({
  sessionId: session.sessionId,
  userId:    user.id,
  updates:   { fullName: 'Alice Johnson', plan: 'enterprise' },
});

// Soft-delete (users can delete themselves; admins can delete anyone)
await auth.deleteUser(session.sessionId, user.id);

The UserRecord shape:

interface UserRecord {
  id:             string;
  email:          string;
  fullName?:      string | null;
  emailVerified:  boolean;
  accountStatus:  'active' | 'locked' | 'suspended';
  role:           'user' | 'admin';
  createdAt:      number;   // Unix ms
  updatedAt:      number;   // Unix ms
  metadata?:      Record<string, unknown>;
  authProvider?:  'email' | 'google';   // how the account was created
  picture?:       string | null;         // Google profile photo URL
  [key: string]:  unknown;               // custom fields from signup
}

Password & Email

// Change password — requires an active session AND the current password
// Pass empty string for currentPassword to SET a first password (Google users)
await auth.changePassword({
  sessionId:       session.sessionId,
  userId:          user.id,
  currentPassword: 'hunter2',
  newPassword:     'correcthorsebatterystaple',
});

// Forgot-password flow
await auth.requestPasswordReset('[email protected]');  // always succeeds (prevents enumeration)
await auth.confirmPasswordReset(tokenFromEmail, 'newpassword123');

// Email verification
await auth.requestEmailVerification(user.id);
await auth.confirmEmailVerification(tokenFromEmail);

Admin Controls

All admin methods require an active admin session.

// Paginated user list
const { users, hasMore, nextCursor } = await auth.listUsers({
  sessionId: adminSession.sessionId,
  limit:     50,
  cursor:    previousNextCursor, // optional — for subsequent pages
});

// Lock an account (default: 15 minutes)
const { lockedUntil, unlockTime } = await auth.lockAccount({
  sessionId: adminSession.sessionId,
  userId:    user.id,
  duration:  60 * 60 * 1000, // optional ms — lock for 1 hour
});

// Unlock an account
await auth.unlockAccount(adminSession.sessionId, user.id);

// Permanent (hard) delete — cannot be undone
await auth.hardDeleteUser(adminSession.sessionId, user.id);

// Bulk delete — soft or hard
const { succeeded, failed } = await auth.bulkDeleteUsers({
  sessionId: adminSession.sessionId,
  userIds:   ['id1', 'id2', 'id3'],
  hard:      true, // optional — defaults to soft delete
});

File Storage

Files are private by default and scoped to your storage key server-side.

const storage = db.storage('main'); // 'main' matches a key in storageKeys config

Upload

// Simple upload — anything up to 500 MB
const result = await storage.upload(file, 'avatars/alice.jpg', {
  isPublic:        true,     // public CDN URL (default: false)
  overwrite:       true,     // replace if the path exists (default: false)
  mimeType:        'image/jpeg',  // optional — auto-detected from content if omitted
  expiresInSeconds: 3600,    // optional — auto-delete after N seconds
});
console.log(result.publicUrl);   // permanent CDN URL (if isPublic: true)
console.log(result.downloadUrl); // authenticated URL (if private)
console.log(result.path);
console.log(result.size);
console.log(result.mimeType);

// Upload a JS object or string directly as a file
await storage.uploadRaw({ theme: 'dark', lang: 'en' }, 'settings/config.json');
await storage.uploadRaw('<html>…</html>', 'pages/home.html', { mimeType: 'text/html' });

Large Files with Progress

Recommended for files > 10 MB or when you need a progress indicator (supported in both browser and Node.js environments).

// Step 1 — get a signed GCS PUT URL
const { uploadUrl, path, expiresAt, expiresIn } = await storage.getUploadUrl({
  path:             'videos/intro.mp4',
  mimeType:         'video/mp4',
  size:             file.size,
  isPublic:         true,
  overwrite:        false,
  expiresInSeconds: 3600, // URL lifetime
});

// Step 2 — upload directly to GCS with progress callback (browser & Node.js)
await storage.uploadToSignedUrl(uploadUrl, file, 'video/mp4', (percent) => {
  console.log(`${percent}% uploaded`);
});

// Step 3 — confirm and register metadata server-side
const result = await storage.confirmUpload({
  path,
  mimeType: 'video/mp4',
  isPublic: true,
});

Batch Uploads

Get signed URLs for up to 50 files at once, upload them in parallel, then confirm in one call.

// Step 1 — get signed URLs for multiple files
const { files: urls } = await storage.getBatchUploadUrls([
  { path: 'docs/a.pdf', mimeType: 'application/pdf', size: fileA.size, isPublic: false },
  { path: 'docs/b.pdf', mimeType: 'application/pdf', size: fileB.size, isPublic: false },
]);

// Step 2 — upload each file (run in parallel)
await Promise.all(
  urls.map(({ uploadUrl, path }, i) =>
    storage.uploadToSignedUrl(uploadUrl, files[i], 'application/pdf'),
  ),
);

// Step 3 — confirm all at once
const { succeeded, failed } = await storage.batchConfirmUploads([
  { path: 'docs/a.pdf', mimeType: 'application/pdf' },
  { path: 'docs/b.pdf', mimeType: 'application/pdf' },
]);

Download

// Download a private file as ArrayBuffer
const buffer = await storage.download('private/report.pdf');

// Convert to a Blob for use in the browser
const blob = new Blob([buffer], { type: 'application/pdf' });
const url  = URL.createObjectURL(blob);

For public files, just use the publicUrl directly — no SDK or authentication needed.


Batch Download

Downloads up to 20 files at once. Content is returned as base64-encoded strings.

const { succeeded, failed } = await storage.batchDownload([
  'docs/report.pdf',
  'images/chart.png',
]);

for (const file of succeeded) {
  console.log(file.path, file.mimeType, file.size);
  // file.content is base64 — decode with atob() or Buffer.from(content, 'base64')
}

for (const err of failed) {
  console.error(err.path, err.error, err.code);
}

List & Metadata

// List files and folders (paginated)
const { files, folders, hasMore, nextCursor } = await storage.list({
  prefix:    'avatars/',  // optional path prefix to list under
  limit:     50,
  cursor:    previousNextCursor,
  recursive: true,  // include files in sub-folders (default: false)
});

// Page 2
if (hasMore) {
  const page2 = await storage.list({ prefix: 'avatars/', cursor: nextCursor });
}

// Get file metadata
const meta = await storage.getMetadata('avatars/alice.jpg');
// → { path, size, mimeType, isPublic, publicUrl, downloadUrl, createdAt, updatedAt }

Temporary Sharing (Signed URLs)

Generate time-limited download URLs for temporary, unauthenticated sharing of private files. Anyone with the URL can access and download the file directly from Google Cloud Storage without needing an X-Storage-Key header.

Note: Downloads via signed URLs bypass the Hydrous server entirely (client direct to GCS), meaning download statistics and monthly bandwidth quotas will not be tracked on the server for these requests.

const { signedUrl, expiresAt, expiresIn } = await storage.getSignedUrl(
  'private/report.pdf',
  3600, // URL lifetime in seconds (default: 3600 = 1 hour, max: 604800 = 7 days)
);

console.log(signedUrl); // Direct GCS download URL
console.log(expiresAt); // ISO timestamp of when the link expires

Move, Copy & Delete

await storage.move('old/path.jpg', 'new/path.jpg');
await storage.copy('templates/base.html', 'pages/home.html');
await storage.deleteFile('avatars/old.jpg');
await storage.deleteFolder('temp/');  // recursively deletes all contents

Visibility

// Make a private file public
const result = await storage.setVisibility('reports/q1.pdf', true);
console.log(result.publicUrl);

// Make a public file private
await storage.setVisibility('reports/q1.pdf', false);

Folders

// Create an explicit folder marker (usually not needed — folders are implicit)
await storage.createFolder('projects/2025/');

Scoped Storage

Automatically prefix every path — ideal for per-user file isolation.

const userFiles = db.storage('main').scope(`users/${userId}/`);

await userFiles.upload(pdf, 'contract.pdf');      // → users/{userId}/contract.pdf
await userFiles.uploadRaw({ key: 'val' }, 'prefs.json');
const { files } = await userFiles.list();          // → only lists users/{userId}/

// Nest deeper
const reports = userFiles.scope('reports/');       // → users/{userId}/reports/
await reports.upload(file, 'q1.pdf');              // → users/{userId}/reports/q1.pdf

// All StorageManager methods are available on ScopedStorage
const meta          = await userFiles.getMetadata('contract.pdf');
const { signedUrl } = await userFiles.getSignedUrl('contract.pdf', 900);
await userFiles.move('old.pdf', 'new.pdf');
await userFiles.deleteFile('contract.pdf');
await userFiles.deleteFolder('reports/');

Storage Stats

// Stats for this storage key
const stats = await storage.getStats();
// → { totalFiles, totalBytes, uploadCount, downloadCount, deleteCount }

// Server info (no auth required)
const info = await storage.info();
// → { ok: true, storageRoot: '…' }

Analytics

BigQuery-powered aggregations. No ETL, no pipelines — just query.

const analytics = db.analytics('orders');

Date Range (Time Scope)

Almost every analytics method accepts an optional dateRange to restrict results to a time window. Both start and end are Unix timestamps in milliseconds. Both fields are optional; omit either for an open-ended range.

interface DateRange {
  start?: number;  // Unix ms — inclusive lower bound
  end?:   number;  // Unix ms — inclusive upper bound
}

Granularity options (for time series methods):

| Value | Buckets results by | |---|---| | 'hour' | Each hour | | 'day' | Each calendar day | | 'week' | Each week | | 'month' | Each month | | 'year' | Each year |

Aggregation options (for numeric field methods):

| Value | Meaning | |---|---| | 'sum' | Total | | 'avg' | Average | | 'min' | Minimum | | 'max' | Maximum | | 'count' | Record count |


Count

// All-time total
const { count } = await analytics.count();

// Within a time window
const { count: lastWeek } = await analytics.count({
  dateRange: {
    start: Date.now() - 7 * 24 * 60 * 60 * 1000,
    end:   Date.now(),
  },
});

Distribution

How many records have each value of a field.

const dist = await analytics.distribution({
  field:     'status',
  limit:     10,
  order:     'desc',
  dateRange: { start: new Date('2025-01-01').getTime() },
});
// → [{ value: 'published', count: 320 }, { value: 'draft', count: 80 }, …]

Sum

Sum a numeric field, optionally grouped by another field.

// Total revenue
const [{ sum: totalRevenue }] = await analytics.sum({ field: 'amount' });

// Revenue by region, last quarter
const sums = await analytics.sum({
  field:     'amount',
  groupBy:   'region',
  limit:     20,
  dateRange: {
    start: new Date('2025-01-01').getTime(),
    end:   new Date('2025-03-31').getTime(),
  },
});
// → [{ group: 'Europe', sum: 18200 }, { group: 'Americas', sum: 29400 }, …]

Time Series

Count of records over time — useful for activity graphs.

// Daily record counts, last 30 days
const daily = await analytics.timeSeries({
  granularity: 'day',
  dateRange: {
    start: Date.now() - 30 * 24 * 60 * 60 * 1000,
    end:   Date.now(),
  },
});
// → [{ date: '2025-03-01', count: 42 }, { date: '2025-03-02', count: 55 }, …]

// Monthly, all time
const monthly = await analytics.timeSeries({ granularity: 'month' });

Field Time Series

Aggregate a numeric field over time — useful for revenue, score, or usage trends.

// Daily sum of revenue, last 90 days
const revTrend = await analytics.fieldTimeSeries({
  field:       'amount',
  aggregation: 'sum',
  granularity: 'day',
  dateRange: {
    start: Date.now() - 90 * 24 * 60 * 60 * 1000,
    end:   Date.now(),
  },
});

// Monthly average order value
const avgTrend = await analytics.fieldTimeSeries({
  field:       'amount',
  aggregation: 'avg',
  granularity: 'month',
});

Top N

Most frequent values for a field by record count.

const top10 = await analytics.topN({
  field:      'countryCode',
  n:          10,
  labelField: 'countryName',
  order:      'desc',
  dateRange:  { start: new Date('2025-01-01').getTime() },
});
// → [{ value: 'US', label: 'United States', count: 420 }, …]

Stats

Statistical summary for a numeric field.

const priceStats = await analytics.stats({
  field:     'price',
  dateRange: { start: new Date('2025-01-01').getTime() },
});
// → { min, max, avg, sum, count, stddev }

Records via BigQuery

Fetch filtered records through the BigQuery engine instead of the GCS index. Useful for large result sets or complex server-side filtering.

const records = await analytics.records<Order>({
  filters: [
    { field: 'status',  op: '==',       value: 'paid' },
    { field: 'amount',  op: '>=',       value: 100 },
    { field: 'country', op: 'CONTAINS', value: 'US' },
  ],
  selectFields: ['id', 'amount', 'country', 'createdAt'],
  orderBy:      'createdAt',
  order:        'desc',
  limit:        1000,
  offset:       0,
  dateRange: {
    start: new Date('2025-01-01').getTime(),
    end:   new Date('2025-03-31').getTime(),
  },
});

Analytics filter operators: == != > < >= <= CONTAINS


Multi-Metric

Compute multiple aggregations in a single round-trip — perfect for dashboards.

const dashboard = await analytics.multiMetric({
  metrics: [
    { field: 'amount',   name: 'totalRevenue', aggregation: 'sum' },
    { field: 'amount',   name: 'avgOrder',     aggregation: 'avg' },
    { field: 'amount',   name: 'maxOrder',     aggregation: 'max' },
    { field: 'recordId', name: 'orderCount',   aggregation: 'count' },
  ],
  dateRange: { start: Date.now() - 30 * 24 * 60 * 60 * 1000 },
});
// → { totalRevenue: 48200, avgOrder: 96.4, maxOrder: 999, orderCount: 500 }

Storage Stats (Analytics)

Record count and byte statistics for the bucket.

const storageInfo = await analytics.storageStats({
  dateRange: { start: new Date('2025-01-01').getTime() },
});
// → { totalRecords, totalBytes, avgBytes, minBytes, maxBytes }

Cross-Bucket

Compare the same metric across multiple buckets in a single query.

const compare = await analytics.crossBucket({
  bucketKeys:  ['orders-2024', 'orders-2025'],
  field:       'amount',
  aggregation: 'sum',
  dateRange:   { start: new Date('2025-01-01').getTime() },
});
// → [{ bucket: 'orders-2024', value: 38200 }, { bucket: 'orders-2025', value: 52100 }]

Raw Query

Escape hatch for query types not covered by the typed helpers.

import type { AnalyticsQuery, AnalyticsResult } from 'hydrousdb';

const result = await analytics.query<MyResultType>({
  queryType:   'distribution',
  field:       'category',
  granularity: 'month',
  filters:     [{ field: 'active', op: '==', value: true }],
  dateRange:   { start: Date.now() - 30 * 24 * 60 * 60 * 1000 },
  limit:       50,
  order:       'desc',
});

Error Handling

import {
  HydrousError,
  AuthError,
  RecordError,
  StorageError,
  AnalyticsError,
  ValidationError,
  NetworkError,
} from 'hydrousdb';

try {
  await db.auth().login({ email: '[email protected]', password: 'wrong' });
} catch (err) {
  if (err instanceof AuthError) {
    console.log(err.code);      // 'INVALID_CREDENTIALS'
    console.log(err.status);    // 401
    console.log(err.message);   // human-readable
    console.log(err.requestId); // for support
  }

  if (err instanceof ValidationError) {
    console.log(err.details);   // string[] of specific validation failures
  }

  if (err instanceof NetworkError) {
    console.log('No internet or server unreachable');
    console.log(err.cause);     // original error
  }
}

| Error class | When it's thrown | |---|---| | HydrousError | Base class — all SDK errors extend this. Has code, status, requestId, details. | | AuthError | Login failures, invalid/expired sessions, permission denied | | RecordError | Record not found, write validation failures | | StorageError | Upload/download failures, file not found | | AnalyticsError | Invalid query, bucket not found | | ValidationError | Bad input caught client-side before the request is sent. Has details: string[]. | | NetworkError | No network, server unreachable, request timed out. Has cause. |


TypeScript

The SDK is written in TypeScript. Type your records for full autocomplete and safety:

interface Order {
  customerId: string;
  amount:     number;
  status:     'pending' | 'paid' | 'refunded';
  items:      { sku: string; qty: number }[];
}

const orders = db.records<Order>('orders');

const order = await orders.create({
  customerId: 'cust_123',
  amount:     49.99,
  status:     'pending',
  items:      [{ sku: 'SHOE-42', qty: 1 }],
});
// order.status → 'pending' | 'paid' | 'refunded'  ✓
// order.id, order.createdAt, order.updatedAt are automatically added ✓

const result = await orders.query({
  filters: [{ field: 'status', op: '==', value: 'paid' }],
});
// result.records → (Order & RecordResult)[]  ✓

Key exported types:

import type {
  HydrousConfig,
  RecordData, RecordResult, QueryOptions, QueryFilter, QueryResult,
  DateRange, Granularity, Aggregation, SortOrder,
  AnalyticsQuery, AnalyticsResult, AnalyticsFilter,
  UserRecord, AuthResult, Session,
  GoogleSignInOptions, GoogleLinkOptions,
  UploadOptions, UploadResult,
  ListOptions, ListResult, FileMetadata, SignedUrlResult,
  StorageStats,
} from 'hydrousdb';

Security

  • Never commit API keys. Use environment variables (process.env.…).
  • Never expose keys in the browser. For client-side apps, proxy all SDK calls through your own backend.
  • Keys travel in headers only — the SDK enforces this. They never appear in URLs, query strings, access logs, or browser history.
  • Files are private by default. isPublic defaults to false. Use getSignedUrl() for temporary external sharing.
  • Use scoped storage (storage.scope('prefix/')) to isolate files per user and prevent path-traversal bugs.
  • Google ID tokens are verified server-side — the SDK never sends tokens to Google directly after initial sign-in.

Framework Examples

Next.js (App Router)

// lib/db.ts
import { createClient } from 'hydrousdb';
export const db = createClient({
  authKey:           process.env.HYDROUS_AUTH_KEY!,
  bucketSecurityKey: process.env.HYDROUS_BUCKET_KEY!,
  storageKeys:       { main: process.env.HYDROUS_STORAGE_KEY! },
});

// app/api/posts/route.ts
import { db } from '@/lib/db';
export async function GET() {
  const { records } = await db.records('posts').query({
    filters:  [{ field: 'status', op: '==', value: 'published' }],
    orderBy:  'createdAt',
    order:    'desc',
    limit:    10,
  });
  return Response.json(records);
}

React (client-side via your own API)

// Always go through your backend — never put keys in React components
const res  = await fetch('/api/posts');
const data = await res.json();

Vue / Nuxt

// composables/useDb.ts
import { createClient } from 'hydrousdb';
export const db = createClient({
  authKey:           import.meta.env.VITE_HYDROUS_AUTH_KEY,
  bucketSecurityKey: import.meta.env.VITE_HYDROUS_BUCKET_KEY,
  storageKeys:       { main: import.meta.env.VITE_HYDROUS_STORAGE_KEY },
});

React Native

import { createClient } from 'hydrousdb';
// Works out of the box — uses the global fetch available in React Native
const db = createClient({
  authKey:           HYDROUS_AUTH_KEY,
  bucketSecurityKey: HYDROUS_BUCKET_KEY,
  storageKeys:       { main: HYDROUS_STORAGE_KEY },
});

API Reference

createClient(config)

| Field | Type | Required | Description | |---|---|---|---| | authKey | string | ✓ | hk_auth_… — for auth routes | | bucketSecurityKey | string | ✓ | hk_bucket_… — for records & analytics | | storageKeys | { [name]: string } | ✓ | One or more ssk_… keys for file storage | | baseUrl | string | — | Override the API endpoint (no trailing slash) |


db.records<T>(bucket) — all methods

| Method | Returns | Description | |---|---|---| | create(data, opts?) | T & RecordResult | Create a record. opts.queryableFields enables filtering. opts.customRecordId enables upsert. | | get(id) | T & RecordResult | Fetch a record by ID. | | patch(id, data, opts?) | { id, updatedAt? } | Partial update. opts.merge for deep merge. opts.trackHistory to save a version. | | delete(id) | void | Permanently delete a record. | | exists(id) | boolean | Lightweight existence check (HEAD request). | | query(opts?) | QueryResult<T> | Filter, sort, paginate. Supports dateRange, timeScope, startDate, endDate, year, sortBy. | | getAll(opts?) | (T & RecordResult)[] | Fetch all records (no filter support — use query for filters). | | batchCreate(items, opts?) | { results, errors, successful, failed } | Up to 500 records at once. | | batchUpdate(updates, userEmail?) | { successful, failed } | Up to 500 records at once. | | batchDelete(ids, userEmail?) | { successful, failed } | Up to 500 records at once. | | getHistory(id) | RecordHistoryEntry[] | List all saved versions. | | getVersion(id, generation) | T & RecordResult | Fetch a specific past version. |

QueryOptions fields:

| Field | Type | Description | |---|---|---| | filters | QueryFilter[] | Array of { field, op, value } | | fields | string | Comma-separated list of fields to return | | orderBy | string | Field to sort by (server maps to sortBy) | | sortBy | string | Alias for orderBy — maps directly to ?sortBy= | | order | 'asc' \| 'desc' | Sort direction | | limit | number | Max records to return | | offset | number | Skip N records | | startAfter | string | Cursor from nextCursor for next-page pagination | | startAt | string | Cursor — include the record at this cursor | | endAt | string | Cursor — stop at this cursor | | dateRange | DateRange | { start?, end? } in Unix ms | | timeScope | string | Prefix-based time filter: _day_YYMMDD, _month_YYMM, or _year_YY | | startDate | string | ISO date string e.g. '2026-01-01' — GCS day-range walk start | | endDate | string | ISO date string e.g. '2026-12-31' — GCS day-range walk end | | year | string | Two-digit year e.g. '26' — restricts monthly walk to that year |


db.auth() — all methods

| Method | Description | |---|---| | signup(opts) | Register + create session. Extra fields on opts are stored on the user. | | login(opts) | Authenticate + create session. | | continueWithGoogle({ idToken }) | Sign in or create account via Google ID token. Returns isNew flag. | | linkGoogle({ sessionId, idToken }) | Add Google sign-in to an existing email/password account. | | unlinkGoogle({ sessionId }) | Remove Google sign-in (only if a password is set). | | logout({ sessionId, allDevices? }) | Revoke one session or all sessions. | | validateSession(sessionId) | Check if a session is active; returns current user. | | refreshSession(refreshToken) | Get a new session from a refresh token. | | getUser(userId) | Fetch a user by ID. | | updateUser(opts) | Update profile fields. | | deleteUser(sessionId, userId) | Soft-delete a user. | | changePassword(opts) | Authenticated password change. Pass currentPassword: '' to set first password on Google accounts. | | requestPasswordReset(email) | Send reset email (always succeeds to prevent enumeration). | | confirmPasswordReset(token, newPw) | Apply new password from reset token. | | requestEmailVerification(userId) | Send verification email. Not needed for Google users. | | confirmEmailVerification(token) | Mark email verified from token. | | listUsers(opts) | Paginated user list. Admin only. | | lockAccount(opts) | Lock a user account. Admin only. | | unlockAccount(sessionId, userId) | Unlock a user account. Admin only. | | hardDeleteUser(sessionId, userId) | Permanent delete. Admin only. | | bulkDeleteUsers(opts) | Delete many users at once (soft or hard). Admin only. |


db.storage(keyName) — all methods

| Method | Description | |---|---| | upload(data, path, opts?) | Server-buffered upload (up to 500 MB). | | uploadRaw(data, path, opts?) | Upload a JS object or string as a file. | | getUploadUrl(opts) | Step 1 of signed-URL upload — get a GCS PUT URL. | | uploadToSignedUrl(url, data, mime, onProgress?) | Step 2 — upload directly to GCS (supports progress in browsers). | | confirmUpload(opts) | Step 3 — register metadata server-side. | | getBatchUploadUrls(files) | Get signed URLs for up to 50 files at once. | | batchConfirmUploads(items) | Confirm multiple direct uploads at once. | | download(path) | Download a private file as ArrayBuffer. | | batchDownload(paths, concurrency?) | Download up to 20 files at once (base64 content). | | list(opts?) | List files and folders. Supports prefix, limit, cursor, recursive. | | getMetadata(path) | File size, MIME type, visibility, URLs. | | getSignedUrl(path, expiresIn?) | Time-limited share link (default 3600 s). | | setVisibility(path, isPublic) | Toggle a file between public and private. | | createFolder(path) | Create an explicit folder marker. | | move(from, to) | Move (rename) a file. | | copy(from, to) | Copy a file to a new path. | | deleteFile(path) | Permanently delete a file. | | deleteFolder(path) | Recursively delete a folder and all its contents. | | getStats() | Upload/download/delete counts and total bytes. | | info() | Server info — no auth required. | | scope(prefix) | Get a ScopedStorage that auto-prefixes all paths. |


db.analytics(bucket) — all methods

| Method | Returns | Description | |---|---|---| | count(opts?) | { count } | Total record count, optionally within a dateRange. | | distribution(opts) | DistributionRow[] | Per-value counts for a field. | | sum(opts) | SumRow[] | Sum a numeric field, optional groupBy. | | timeSeries(opts?) | TimeSeriesRow[] | Record counts bucketed by granularity. | | fieldTimeSeries(opts) | FieldTimeSeriesRow[] | Numeric field aggregated over time. | | topN(opts) | TopNRow[] | Most frequent field values. | | stats(opts) | FieldStats | min / max / avg / sum / count / stddev. | | records(opts?) | (T & RecordResult)[] | Filtered records via BigQuery (good for large sets). | | multiMetric(opts) | MultiMetricResult | Multiple aggregations in one request. | | storageStats(opts?) | StorageStatsResult | Record count and byte stats for the bucket. | | crossBucket(opts) | CrossBucketRow[] | Compare a metric across multiple buckets. | | query(query) | AnalyticsResult<T> | Raw escape hatch for any AnalyticsQuery. |


Contributing

git clone https://github.com/hydrousdb/hydrousdb-js.git
cd hydrousdb-js
npm install
npm test        # run tests
npm run build   # compile

License

MIT — LICENSE