Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,7 @@ That's it! 🎉

## 🟢 Why urBackend?

While tools like PocketBase are great for small single-server apps (SQLite), **urBackend** is built for scale.

| Feature | urBackend | Firebase | PocketBase |
| :--- | :--- | :--- | :--- |
| **Database** | **MongoDB (Scalable)** | Proprietary NoSQL | SQLite (Single Server) |
| **Caching** | **Redis Built-in** | None | None |
| **Hosting** | **Self-Hosted / Cloud** | Cloud Only | Self-Hosted |
| **Source** | **Open-Source** | Closed-Source | Open-Source |
**urBackend** is built for developer velocity and horizontal scale, combining the simplicity of a modern BaaS with the power of MongoDB and Redis.

### Core Features:
- **Instant NoSQL:** Create collections and push JSON data instantly.
Expand Down
16 changes: 13 additions & 3 deletions apps/dashboard-api/src/middlewares/planEnforcement.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ exports.checkByokGate = async function(req, res, next) {
}

exports.checkWebhookGate = async function(req, res, next) {
const { Project, resolveEffectivePlan, getPlanLimits, AppError, sanitizeObjectId } = require('@urbackend/common');
const { Project, Webhook, resolveEffectivePlan, getPlanLimits, AppError, sanitizeObjectId } = require('@urbackend/common');
try {
if (isAdminRequest(req)) return next();

Expand All @@ -186,8 +186,15 @@ exports.checkWebhookGate = async function(req, res, next) {
return next(new AppError(403, 'Verify your email to create or test webhooks.'));
}

// Only enforce count limit on new webhook creation (POST without webhookId)
const isCreate = req.method === 'POST' && !req.params.webhookId;
if (!isCreate) {
return next();
}

const rawProjectId = req.params.projectId || req.body.projectId || req.query.projectId;
const cleanProjectId = sanitizeObjectId(rawProjectId);
if (!cleanProjectId) return next(new AppError(400, 'Invalid or missing projectId'));

let customLimits = null;
if (cleanProjectId) {
Expand All @@ -198,8 +205,11 @@ exports.checkWebhookGate = async function(req, res, next) {
const effectivePlan = resolveEffectivePlan(req.developer);
const limits = getPlanLimits({ plan: effectivePlan, customLimits });

if (limits.webhooksLimit === 0) {
return next(new AppError(403, 'Webhooks are a Pro feature. Please upgrade to create integrations.'));
if (limits.webhooksLimit !== -1) {
const currentCount = await Webhook.countDocuments({ projectId: cleanProjectId });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if (currentCount >= limits.webhooksLimit) {
return next(new AppError(403, `Webhook limit reached (${limits.webhooksLimit}). Please upgrade your plan for unlimited webhooks.`));
}
}

next();
Expand Down
32 changes: 15 additions & 17 deletions apps/landing/src/pages/pricing.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,27 @@ import Footer from '../components/Footer.astro';
import { Check, ChevronDown, HelpCircle, ArrowRight } from 'lucide-react';

const FREE_FEATURES = [
'1 Project',
'5 Collections per project',
'1 Project, 5 Collections',
'2,000 API requests / day',
'10MB File Storage',
'200 Auth Users',
'25 Emails / month',
'Global email templates',
'3 Active Webhooks',
'50 Emails / month (Sends & Broadcasts)',
'Custom HTML Email Templates',
'Bring Your Own MongoDB (BYOM)',
'Bring Your Own Storage (S3 / R2)',
'Bring Your Own Keys (Resend, OAuth, AI)',
'Team Collaboration (up to 2 members)',
'Community support',
];

const PRO_FEATURES = [
'10 Projects',
'Unlimited collections',
'Unlimited API requests',
'10 Projects, Unlimited Collections',
'Unlimited API requests (600 req/min burst)',
'Unlimited Auth Users',
'Unlimited Webhooks',
'External Database (BYOM)',
'Bring your own Storage (S3/R2)',
'Unlimited Webhooks + Priority Delivery',
'1,000 Emails / month',
'Custom HTML email templates',
'BYOK — own API keys',
'Analytics Pro',
'AI integrations (OpenAI, Groq)',
'Up to 6 Team Members',
'Analytics Pro & Deep Insights',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
'Priority support',
];

Expand All @@ -38,15 +36,15 @@ const PRICING_FAQS = [
},
{
q: 'Can I bring my own MongoDB Atlas cluster on the Free tier?',
a: 'The Free tier includes urBackend managed database storage. Bring Your Own MongoDB (BYOM) is available on the Pro tier during beta.'
a: 'Yes! Both Free and Pro tiers support Bring Your Own MongoDB (BYOM) and Bring Your Own Storage (BYOS). Connect your Atlas cluster or S3 bucket directly on any plan.'
},
{
q: 'How does the free 1-month Pro trial work?',
a: 'Click "Get 1 month Pro for free" to unlock Pro features on your account with zero credit card required.'
},
{
q: 'Do you offer team collaboration?',
a: 'Yes. Pro plans support inviting team members with granular project access controls and role-based permissions.'
a: 'Yes. Free plans allow up to 2 team members (owner + 1 collaborator). Pro plans support up to 6 members with role-based access control.'
}
];
---
Expand Down
23 changes: 20 additions & 3 deletions apps/public-api/src/controllers/mail.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -727,12 +727,12 @@ module.exports.deleteContact = async (req, res, next) => {
}
};

// --- BROADCASTS (BYOK + Pro Gate) ---
// --- BROADCASTS (BYOK Gate + Quota Tracking) ---

const requireBroadcastGate = async (req) => {
const { resend, usingByok } = await resolveResendClient(req);
if (!usingByok || !req.planLimits?.byokEnabled) {
const err = new Error("Broadcasts require both a BYOK Resend key and a Pro plan.");
const err = new Error("Broadcasts require a configured BYOK Resend key.");
err.statusCode = 403;
throw err;
}
Expand Down Expand Up @@ -766,17 +766,34 @@ module.exports.createBroadcast = async (req, res, next) => {
};

module.exports.sendBroadcast = async (req, res, next) => {
let consumedQuotaKey = null;
try {
const { id } = req.params;
if (!/^[A-Za-z0-9_-]+$/.test(id)) {
return next(new AppError(400, "Invalid broadcast ID format."));
}
const resend = await requireBroadcastGate(req);

const projectId = req.project?._id?.toString() || req.project?._id;
if (projectId) {
const limit = getMonthlyMailLimit(req.project, req.planLimits);
const { count, key } = await reserveMonthlyMailSlot(projectId, limit);
consumedQuotaKey = key;
}

const { data, error } = await resend.broadcasts.send(id);
if (error) return next(new AppError(error.statusCode || 500, error.message));
if (error) {
if (consumedQuotaKey) {
await redis.decr(consumedQuotaKey).catch(() => {});
}
return next(new AppError(error.statusCode || 500, error.message));
}

return new ApiResponse(data).send(res, 200);
} catch (err) {
if (consumedQuotaKey) {
await redis.decr(consumedQuotaKey).catch(() => {});
}
return next(new AppError(err.statusCode || 500, err.message));
}
};
Expand Down
10 changes: 7 additions & 3 deletions apps/web-dashboard/src/components/Dashboard/StatsRow.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import React from 'react';
import { Folder, Activity, Zap } from 'lucide-react';
import StatCard from './StatCard';
import { useAuth } from '../../context/AuthContext';

const StatsRow = ({ projectsCount }) => {
const { user } = useAuth();
const isPro = user?.plan === 'pro';

return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '1.5rem', marginBottom: '4rem' }}>
<StatCard
Expand All @@ -22,10 +26,10 @@ const StatsRow = ({ projectsCount }) => {
/>
<StatCard
title="Current Plan"
value="Free Tier"
value={isPro ? "Pro Tier" : "Free Tier"}
icon={Zap}
color="#FFBD2E"
background="rgba(255, 189, 46, 0.1)"
color={isPro ? "var(--color-primary, #3ECF8E)" : "#FFBD2E"}
background={isPro ? "rgba(62, 207, 142, 0.1)" : "rgba(255, 189, 46, 0.1)"}
/>
</div>
);
Expand Down
8 changes: 4 additions & 4 deletions apps/web-dashboard/src/components/Dashboard/UsageQuota.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const UsageQuota = () => {
<UsageProgressBar
label="Webhooks"
used={usage?.totalWebhooks ?? 0}
limit={limits?.webhooksLimit ?? 0}
limit={limits?.webhooksLimit ?? 3}
unit="Hooks"
unlimited={limits?.webhooksLimit === -1}
tooltip={limits?.webhooksLimit === -1 ? "Unlimited webhooks included in your plan" : ""}
Expand All @@ -87,8 +87,8 @@ const UsageQuota = () => {
limit={limits?.storageBytes ?? 10485760}
formatValue={formatBytes}
unit=""
unlimited={limits?.storageBytes === -1}
tooltip={limits?.storageBytes === -1 ? "Bring Your Own Storage (BYOS) enabled: Connect an external storage provider for unlimited storage." : ""}
unlimited={limits?.storageBytes === -1 || limits?.byosEnabled}
tooltip={limits?.byosEnabled ? "Bring Your Own Storage (BYOS) enabled: Connect an external storage provider for unlimited storage." : (limits?.storageBytes === -1 ? "Unlimited managed file storage" : "")}
/>
<UsageProgressBar
label="Requests"
Expand All @@ -103,7 +103,7 @@ const UsageQuota = () => {
{!isPro && (
<button
id="usage-quota-upgrade-btn"
onClick={() => window.location.href = 'https://urbackend.bitbros.in/pricing'}
onClick={() => window.location.href = 'https://urbackend.in/pricing'}
className="btn btn-secondary"
style={{ marginTop: '0.5rem', width: '100%', fontSize: '0.75rem', padding: '5px' }}
>
Expand Down
28 changes: 13 additions & 15 deletions apps/web-dashboard/src/components/UpgradeModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,27 @@ import { X, Zap, Check, Loader2 } from 'lucide-react';
// import { useAuth } from '../context/AuthContext';

const FREE_FEATURES = [
'1 Project',
'5 Collections per project',
'1 Project, 5 Collections',
'2,000 API requests / day',
'10MB File Storage',
'200 Auth Users',
'25 Emails / month',
'Global email templates',
'3 Active Webhooks',
'50 Emails / month (Sends & Broadcasts)',
'Custom HTML Email Templates',
'Bring Your Own MongoDB (BYOM)',
'Bring Your Own Storage (S3 / R2)',
'Bring Your Own Keys (Resend, OAuth, AI)',
'Team Collaboration (up to 2 members)',
'Community support',
];

const PRO_FEATURES = [
'10 Projects',
'Unlimited collections',
'Unlimited API requests',
'10 Projects, Unlimited Collections',
'Unlimited API requests (600 req/min burst)',
'Unlimited Auth Users',
'Unlimited Webhooks',
'External Database (BYOM)',
'Bring your own Storage (S3/R2)',
'Unlimited Webhooks + Priority Delivery',
'1,000 Emails / month',
'Custom HTML email templates',
'BYOK — own API keys',
'Analytics Pro',
'AI integrations (OpenAI, Groq)',
'Up to 6 Team Members',
'Analytics Pro & Deep Insights',
'Priority support',
];

Expand Down
2 changes: 1 addition & 1 deletion apps/web-dashboard/src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export default function Dashboard() {
</span>
</div>
<button
onClick={() => window.location.href = 'https://urbackend.bitbros.in/pricing'}
onClick={() => window.location.href = 'https://urbackend.in/pricing'}
className="btn btn-secondary"
style={{ fontSize: '0.75rem', padding: '3px 10px', whiteSpace: 'nowrap', flexShrink: 0 }}
>
Expand Down
2 changes: 1 addition & 1 deletion apps/web-dashboard/src/pages/Docs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ console.log("File URL:", result.url);

<h3 style={{ fontSize: '1.1rem', marginBottom: '1rem' }}>Storage & Database Quotas</h3>
<p style={{ color: '#aaa', lineHeight: '1.6', marginBottom: '1rem' }}>
Total allowed usage is determined by your current plan. By default on the Free tier, projects have caps on total Database Documents and File Storage. Exceeding these returns a <code>403 Forbidden</code> response on POST/PUT requests.
Total allowed usage is determined by your current plan. On the Free tier, projects have managed caps on Database Storage (50 MB) and File Storage (10 MB). Connect your own MongoDB Atlas (BYOM) or external S3/R2 storage (BYOS) to unlock unlimited storage. Exceeding managed quotas returns a <code>403 Forbidden</code> response on POST/PUT requests.
</p>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/web-dashboard/src/utils/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ api.interceptors.response.use(

if (isPlanError && !isVerificationGate && !requiresByok) {
toast.error("Plan limit reached. Please upgrade to continue.");
if (window.location.href !== 'https://urbackend.bitbros.in/pricing') {
window.location.assign('https://urbackend.bitbros.in/pricing');
if (window.location.href !== 'https://urbackend.in/pricing') {
window.location.assign('https://urbackend.in/pricing');
}
return Promise.reject(error);
} else {
Expand Down
2 changes: 1 addition & 1 deletion mintlify/docs/api-reference/storage/upload.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Send the request as `multipart/form-data` with a single field:
| Limit | Value |
| :--- | :--- |
| Max file size | 10 MB per file (urBackend-hosted storage only) |
| Total storage quota | 100 MB (default plan) |
| Total storage quota | 10 MB (Free plan; unlimited with BYOS) |

Projects connected to external (bring-your-own) storage are not subject to the 10 MB per-file limit.

Expand Down
1 change: 0 additions & 1 deletion mintlify/docs/guides/ai-byok.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ Pass `"groqKey": null` to clear the project's key.
| Condition | Status | Detail |
| :--- | :--- | :--- |
| Key does not start with `gsk_`, or is longer than 200 characters | `400` | `Invalid Groq API key format. Key must start with 'gsk_'.` |
| Saving a key on a plan without BYOK for AI enabled | `403` | `Bring Your Own AI Key (Groq) is a Pro feature. Please upgrade to continue.` |
| Project ID is not a valid ObjectId | `400` | `Invalid project ID` |
| Project not found or caller is not an admin | `404` | `Project not found or access denied` |
| Groq rejects the key at request time | `401` | `Invalid BYOK Groq key provided` |
Expand Down
4 changes: 2 additions & 2 deletions mintlify/docs/guides/mail-platform.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ flowchart LR
| Batch send (`/api/mail/send-batch`, max 100 items) | ✅ | ✅ | ✅ |
| Delivery logs + live status | ✅ | ✅ | ✅ |
| Audiences & Contacts | ❌ | ✅ | ✅ |
| Marketing Broadcasts | ❌ | ❌ | ✅ |
| Marketing Broadcasts | ❌ | ✅ (within 50/mo limit) | ✅ (within 1,000/mo limit) |

## Getting started

Expand Down Expand Up @@ -604,7 +604,7 @@ Add product screenshots/annotated walkthrough images in this section if your doc
| `GET/PATCH/DELETE /api/mail/audiences/:id/contacts/*` | `400` | Invalid audience/contact id format |
| `GET/PATCH/DELETE /api/mail/audiences/:id/contacts/*` | `403` | Contacts require a custom Resend API Key (BYOK) |
| `POST /api/mail/broadcasts*` | `400` | audienceId, subject, and html are required |
| `POST /api/mail/broadcasts*` | `403` | Broadcasts require both BYOK and Pro plan |
| `POST /api/mail/broadcasts*` | `403` | Broadcasts require a configured BYOK Resend key |
| `POST /api/mail/webhook` | `400` | Webhook signature verification failed |
| Provider-proxied endpoints | passthrough | Upstream Resend status/message are surfaced |

Expand Down
4 changes: 2 additions & 2 deletions mintlify/docs/guides/storage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Uploads use a presigned URL three-step flow so the binary is sent directly to st
| `filePath` | Opaque storage path (format: `projectId/uuid_sanitizedFilename`) — save this to confirm or delete the file later |

<Note>
This endpoint enforces the 10 MB max file size and checks available headroom against your plan's project storage quota before the upload is confirmed. On the free tier, the default project storage limit is 20 MB. If your project uses external (bring-your-own) storage, the 10 MB per-file cap and project quota checks do not apply.
This endpoint enforces the 10 MB max file size and checks available headroom against your plan's project storage quota before the upload is confirmed. On the free tier, the default project storage limit is 10 MB. If your project uses external (bring-your-own) storage, the 10 MB per-file cap and project quota checks do not apply.
</Note>

### Step 2 — Upload the binary to storage
Expand Down Expand Up @@ -229,7 +229,7 @@ If the path is invalid or the file has already been removed, the API returns `40
| Limit | Value |
| --- | --- |
| Maximum file size | 10 MB per file (urBackend-hosted storage only) |
| Total storage per project | 20 MB (free tier; higher limits on paid plans) |
| Total storage per project | 10 MB (Free tier; unlimited with external BYOS) |

Projects connected to external (bring-your-own) storage are not subject to the 10 MB per-file limit.

Expand Down
4 changes: 2 additions & 2 deletions mintlify/docs/limits-and-quotas.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ Requests that exceed the limit receive a `429 Too Many Requests` response. Desig
| Resource | Limit |
| :--- | :--- |
| File upload size | 10 MB per file (urBackend-hosted storage only) |
| Storage quota per project | 20 MB |
| Storage quota per project | 10 MB (Free tier; unlimited with BYOS) |

Files that exceed the limit return `413 Payload Too Large`. Projects connected to external (bring-your-own) storage are not subject to the 10 MB per-file limit.

## Database limits

| Resource | Limit |
| :--- | :--- |
| Database size per project | 20 MB |
| Database size per project | 50 MB (Free tier; unlimited with BYOM) |
| Documents per read request (`limit`) | 100 by default, up to 1000 |

When your project approaches the database quota, writes may be rejected with `403 Forbidden`. Monitor your usage in the Dashboard.
Expand Down
2 changes: 1 addition & 1 deletion mintlify/docs/sdk/storage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ deleteFile(path: string): Promise<{ deleted: boolean }>
| Limit | Value |
|---|---|
| Max file size | 10 MB per file (urBackend-hosted storage only) |
| Total storage per project | 20 MB (Free Tier) |
| Total storage per project | 10 MB (Free Tier; unlimited with BYOS) |

<Note>
Uploads that exceed 10 MB are rejected with a `StorageError`. Projects connected to external (bring-your-own) storage are not subject to the 10 MB per-file limit.
Expand Down
Loading
Loading