saas-backend-kit 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,203 +0,0 @@
1
- import express from 'express';
2
- import {
3
- auth,
4
- rateLimit,
5
- logger,
6
- config,
7
- queue,
8
- notify,
9
- response,
10
- createApp
11
- } from '../src';
12
-
13
- config.load();
14
-
15
- const app = express();
16
-
17
- app.use(express.json());
18
-
19
- const authMiddleware = auth.initialize({
20
- jwtSecret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
21
- jwtExpiresIn: '7d',
22
- refreshExpiresIn: '30d',
23
- googleClientId: process.env.GOOGLE_CLIENT_ID,
24
- googleClientSecret: process.env.GOOGLE_CLIENT_SECRET,
25
- googleRedirectUri: process.env.GOOGLE_REDIRECT_URI,
26
- });
27
-
28
- app.use(authMiddleware);
29
-
30
- app.use(rateLimit({
31
- window: '1m',
32
- limit: 100,
33
- }));
34
-
35
- app.post('/auth/register', async (req, res) => {
36
- try {
37
- const { email, password, name } = req.body;
38
- const result = await auth().register({ email, password, name });
39
- res.success(result.tokens, 'Registration successful');
40
- } catch (error: any) {
41
- res.badRequest(error.message);
42
- }
43
- });
44
-
45
- app.post('/auth/login', async (req, res) => {
46
- try {
47
- const { email, password } = req.body;
48
- const result = await auth().login({ email, password });
49
- res.success(result.tokens, 'Login successful');
50
- } catch (error: any) {
51
- res.unauthorized('Invalid credentials');
52
- }
53
- });
54
-
55
- app.post('/auth/refresh', async (req, res) => {
56
- try {
57
- const { refreshToken } = req.body;
58
- const tokens = await auth().refresh(refreshToken);
59
- res.success(tokens);
60
- } catch (error: any) {
61
- res.unauthorized('Invalid refresh token');
62
- }
63
- });
64
-
65
- app.get('/auth/google', async (req, res) => {
66
- const url = await auth().getGoogleAuthUrl();
67
- res.redirect(url);
68
- });
69
-
70
- app.get('/auth/google/callback', async (req, res) => {
71
- try {
72
- const { code } = req.query;
73
- const result = await auth().handleGoogleCallback(code as string);
74
- res.success(result.tokens, 'Google login successful');
75
- } catch (error: any) {
76
- res.badRequest('Google authentication failed');
77
- }
78
- });
79
-
80
- app.get('/dashboard', auth.requireUser(), (req, res) => {
81
- res.success({ message: 'Welcome to your dashboard', user: req.user });
82
- });
83
-
84
- app.get('/admin', auth.requireRole('admin'), (req, res) => {
85
- res.success({ message: 'Admin area' });
86
- });
87
-
88
- app.get('/protected', auth.requirePermission('read'), (req, res) => {
89
- res.success({ message: 'You have read permission' });
90
- });
91
-
92
- const emailQueue = queue.create('email', {
93
- defaultJobOptions: {
94
- attempts: 3,
95
- backoff: {
96
- type: 'exponential',
97
- delay: 1000,
98
- },
99
- },
100
- });
101
-
102
- emailQueue.add('sendWelcomeEmail', { userId: '123', email: 'user@example.com' }, {
103
- delay: 5000,
104
- });
105
-
106
- queue.process('email', async (job) => {
107
- logger.info(`Processing email job`, { jobId: job.id, type: job.name });
108
-
109
- await notify.email({
110
- to: job.data.email,
111
- subject: 'Welcome!',
112
- template: 'welcome',
113
- templateData: { name: 'User', appName: 'My SaaS App' },
114
- });
115
-
116
- return { sent: true };
117
- });
118
-
119
- app.post('/notify/email', async (req, res) => {
120
- try {
121
- const { to, subject, template, data } = req.body;
122
- await notify.email({ to, subject, template, templateData: data });
123
- res.success({ message: 'Email sent' });
124
- } catch (error: any) {
125
- res.internalError(error.message);
126
- }
127
- });
128
-
129
- app.post('/notify/sms', async (req, res) => {
130
- try {
131
- const { to, message } = req.body;
132
- await notify.sms({ to, message });
133
- res.success({ message: 'SMS sent' });
134
- } catch (error: any) {
135
- res.internalError(error.message);
136
- }
137
- });
138
-
139
- app.post('/notify/webhook', async (req, res) => {
140
- try {
141
- const { url, method, body, headers } = req.body;
142
- const result = await notify.webhook({ url, method, body, headers });
143
- res.success(result);
144
- } catch (error: any) {
145
- res.internalError(error.message);
146
- }
147
- });
148
-
149
- app.post('/notify/slack', async (req, res) => {
150
- try {
151
- const { text, blocks } = req.body;
152
- await notify.slack({ text, blocks });
153
- res.success({ message: 'Slack notification sent' });
154
- } catch (error: any) {
155
- res.internalError(error.message);
156
- }
157
- });
158
-
159
- app.get('/api/users', (req, res) => {
160
- const users = [
161
- { id: '1', name: 'John Doe', email: 'john@example.com' },
162
- { id: '2', name: 'Jane Smith', email: 'jane@example.com' },
163
- ];
164
-
165
- const page = parseInt(req.query.page as string) || 1;
166
- const limit = parseInt(req.query.limit as string) || 10;
167
- const total = users.length;
168
-
169
- res.paginated(users, page, limit, total);
170
- });
171
-
172
- app.get('/api/users/:id', (req, res) => {
173
- const user = { id: req.params.id, name: 'John Doe', email: 'john@example.com' };
174
- res.success(user);
175
- });
176
-
177
- app.post('/api/users', (req, res) => {
178
- const user = { id: '3', ...req.body };
179
- res.created(user, 'User created');
180
- });
181
-
182
- app.put('/api/users/:id', (req, res) => {
183
- const user = { id: req.params.id, ...req.body };
184
- res.updated(user, 'User updated');
185
- });
186
-
187
- app.delete('/api/users/:id', (req, res) => {
188
- res.deleted('User deleted');
189
- });
190
-
191
- const PORT = config.int('PORT') || 3000;
192
-
193
- app.listen(PORT, () => {
194
- logger.info(`Server running on port ${PORT}`);
195
- });
196
-
197
- process.on('SIGTERM', async () => {
198
- logger.info('SIGTERM received, closing server');
199
- await queue.closeAll();
200
- process.exit(0);
201
- });
202
-
203
- export default app;
package/jest-output.json DELETED
@@ -1,72 +0,0 @@
1
- FAIL tests/utils.test.ts (7.974 s)
2
- Utils Module
3
- × should generate random ID (464 ms)
4
- × should generate ID with custom length (330 ms)
5
- × should hash string (18 ms)
6
- × should generate token (18 ms)
7
- × should generate API key with prefix (61 ms)
8
- × should sleep (25 ms)
9
- × should retry on failure (14 ms)
10
- × should parse query int (14 ms)
11
- × should parse query bool (17 ms)
12
-
13
- ● Utils Module › should generate random ID
14
-
15
-
16
-
17
-
18
-
19
- ● Utils Module › should generate ID with custom length
20
-
21
-
22
-
23
-
24
-
25
- ● Utils Module › should hash string
26
-
27
-
28
-
29
-
30
-
31
- ● Utils Module › should generate token
32
-
33
-
34
-
35
-
36
-
37
- ● Utils Module › should generate API key with prefix
38
-
39
-
40
-
41
-
42
-
43
- ● Utils Module › should sleep
44
-
45
-
46
-
47
-
48
-
49
- ● Utils Module › should retry on failure
50
-
51
-
52
-
53
-
54
-
55
- ● Utils Module › should parse query int
56
-
57
-
58
-
59
-
60
-
61
- ● Utils Module › should parse query bool
62
-
63
-
64
-
65
-
66
-
67
- Test Suites: 1 failed, 1 total
68
- Tests: 9 failed, 9 total
69
- Snapshots: 0 total
70
- Time: 8.281 s
71
- Ran all test suites matching /tests\\utils.test.ts/i.
72
- {"numFailedTestSuites":1,"numFailedTests":9,"numPassedTestSuites":0,"numPassedTests":0,"numPendingTestSuites":0,"numPendingTests":0,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":1,"numTotalTests":9,"openHandles":[],"snapshot":{"added":0,"didUpdate":false,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0},"startTime":1772702813609,"success":false,"testResults":[{"assertionResults":[{"ancestorTitles":["Utils Module"],"duration":464,"failureDetails":[{"name":"TSError","diagnosticText":"\u001b[96msrc/utils/index.ts\u001b[0m:\u001b[93m151\u001b[0m:\u001b[93m7\u001b[0m - \u001b[91merror\u001b[0m\u001b[90m TS2322: \u001b[0mType 'unknown' is not assignable to type 'T[keyof T] | undefined'.\n\n\u001b[7m151\u001b[0m sanitized[key as keyof T] = obj[key];\n\u001b[7m \u001b[0m \u001b[91m ~~~~~~~~~~~~~~~~~~~~~~~~~\u001b[0m\n","diagnosticCodes":[2322]}],"failureMessages":[""],"fullName":"Utils Module should generate random ID","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"status":"failed","title":"should generate random ID"},{"ancestorTitles":["Utils Module"],"duration":330,"failureDetails":[{"name":"TSError","diagnosticText":"\u001b[96msrc/utils/index.ts\u001b[0m:\u001b[93m151\u001b[0m:\u001b[93m7\u001b[0m - \u001b[91merror\u001b[0m\u001b[90m TS2322: \u001b[0mType 'unknown' is not assignable to type 'T[keyof T] | undefined'.\n\n\u001b[7m151\u001b[0m sanitized[key as keyof T] = obj[key];\n\u001b[7m \u001b[0m \u001b[91m ~~~~~~~~~~~~~~~~~~~~~~~~~\u001b[0m\n","diagnosticCodes":[2322]}],"failureMessages":[""],"fullName":"Utils Module should generate ID with custom length","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"status":"failed","title":"should generate ID with custom length"},{"ancestorTitles":["Utils Module"],"duration":18,"failureDetails":[{"name":"TSError","diagnosticText":"\u001b[96msrc/utils/index.ts\u001b[0m:\u001b[93m151\u001b[0m:\u001b[93m7\u001b[0m - \u001b[91merror\u001b[0m\u001b[90m TS2322: \u001b[0mType 'unknown' is not assignable to type 'T[keyof T] | undefined'.\n\n\u001b[7m151\u001b[0m sanitized[key as keyof T] = obj[key];\n\u001b[7m \u001b[0m \u001b[91m ~~~~~~~~~~~~~~~~~~~~~~~~~\u001b[0m\n","diagnosticCodes":[2322]}],"failureMessages":[""],"fullName":"Utils Module should hash string","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"status":"failed","title":"should hash string"},{"ancestorTitles":["Utils Module"],"duration":18,"failureDetails":[{"name":"TSError","diagnosticText":"\u001b[96msrc/utils/index.ts\u001b[0m:\u001b[93m151\u001b[0m:\u001b[93m7\u001b[0m - \u001b[91merror\u001b[0m\u001b[90m TS2322: \u001b[0mType 'unknown' is not assignable to type 'T[keyof T] | undefined'.\n\n\u001b[7m151\u001b[0m sanitized[key as keyof T] = obj[key];\n\u001b[7m \u001b[0m \u001b[91m ~~~~~~~~~~~~~~~~~~~~~~~~~\u001b[0m\n","diagnosticCodes":[2322]}],"failureMessages":[""],"fullName":"Utils Module should generate token","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"status":"failed","title":"should generate token"},{"ancestorTitles":["Utils Module"],"duration":61,"failureDetails":[{"name":"TSError","diagnosticText":"\u001b[96msrc/utils/index.ts\u001b[0m:\u001b[93m151\u001b[0m:\u001b[93m7\u001b[0m - \u001b[91merror\u001b[0m\u001b[90m TS2322: \u001b[0mType 'unknown' is not assignable to type 'T[keyof T] | undefined'.\n\n\u001b[7m151\u001b[0m sanitized[key as keyof T] = obj[key];\n\u001b[7m \u001b[0m \u001b[91m ~~~~~~~~~~~~~~~~~~~~~~~~~\u001b[0m\n","diagnosticCodes":[2322]}],"failureMessages":[""],"fullName":"Utils Module should generate API key with prefix","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"status":"failed","title":"should generate API key with prefix"},{"ancestorTitles":["Utils Module"],"duration":25,"failureDetails":[{"name":"TSError","diagnosticText":"\u001b[96msrc/utils/index.ts\u001b[0m:\u001b[93m151\u001b[0m:\u001b[93m7\u001b[0m - \u001b[91merror\u001b[0m\u001b[90m TS2322: \u001b[0mType 'unknown' is not assignable to type 'T[keyof T] | undefined'.\n\n\u001b[7m151\u001b[0m sanitized[key as keyof T] = obj[key];\n\u001b[7m \u001b[0m \u001b[91m ~~~~~~~~~~~~~~~~~~~~~~~~~\u001b[0m\n","diagnosticCodes":[2322]}],"failureMessages":[""],"fullName":"Utils Module should sleep","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"status":"failed","title":"should sleep"},{"ancestorTitles":["Utils Module"],"duration":14,"failureDetails":[{"name":"TSError","diagnosticText":"\u001b[96msrc/utils/index.ts\u001b[0m:\u001b[93m151\u001b[0m:\u001b[93m7\u001b[0m - \u001b[91merror\u001b[0m\u001b[90m TS2322: \u001b[0mType 'unknown' is not assignable to type 'T[keyof T] | undefined'.\n\n\u001b[7m151\u001b[0m sanitized[key as keyof T] = obj[key];\n\u001b[7m \u001b[0m \u001b[91m ~~~~~~~~~~~~~~~~~~~~~~~~~\u001b[0m\n","diagnosticCodes":[2322]}],"failureMessages":[""],"fullName":"Utils Module should retry on failure","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"status":"failed","title":"should retry on failure"},{"ancestorTitles":["Utils Module"],"duration":14,"failureDetails":[{"name":"TSError","diagnosticText":"\u001b[96msrc/utils/index.ts\u001b[0m:\u001b[93m151\u001b[0m:\u001b[93m7\u001b[0m - \u001b[91merror\u001b[0m\u001b[90m TS2322: \u001b[0mType 'unknown' is not assignable to type 'T[keyof T] | undefined'.\n\n\u001b[7m151\u001b[0m sanitized[key as keyof T] = obj[key];\n\u001b[7m \u001b[0m \u001b[91m ~~~~~~~~~~~~~~~~~~~~~~~~~\u001b[0m\n","diagnosticCodes":[2322]}],"failureMessages":[""],"fullName":"Utils Module should parse query int","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"status":"failed","title":"should parse query int"},{"ancestorTitles":["Utils Module"],"duration":17,"failureDetails":[{"name":"TSError","diagnosticText":"\u001b[96msrc/utils/index.ts\u001b[0m:\u001b[93m151\u001b[0m:\u001b[93m7\u001b[0m - \u001b[91merror\u001b[0m\u001b[90m TS2322: \u001b[0mType 'unknown' is not assignable to type 'T[keyof T] | undefined'.\n\n\u001b[7m151\u001b[0m sanitized[key as keyof T] = obj[key];\n\u001b[7m \u001b[0m \u001b[91m ~~~~~~~~~~~~~~~~~~~~~~~~~\u001b[0m\n","diagnosticCodes":[2322]}],"failureMessages":[""],"fullName":"Utils Module should parse query bool","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"status":"failed","title":"should parse query bool"}],"endTime":1772702821883,"message":" ● Utils Module › should generate random ID\n\n\n\n\n\n ● Utils Module › should generate ID with custom length\n\n\n\n\n\n ● Utils Module › should hash string\n\n\n\n\n\n ● Utils Module › should generate token\n\n\n\n\n\n ● Utils Module › should generate API key with prefix\n\n\n\n\n\n ● Utils Module › should sleep\n\n\n\n\n\n ● Utils Module › should retry on failure\n\n\n\n\n\n ● Utils Module › should parse query int\n\n\n\n\n\n ● Utils Module › should parse query bool\n\n\n\n\n","name":"C:\\Users\\ashis\\OneDrive\\Desktop\\test\\open\\saas-backend-kit\\tests\\utils.test.ts","startTime":1772702813909,"status":"failed","summary":""}],"wasInterrupted":false}
package/jest.config.js DELETED
@@ -1,19 +0,0 @@
1
- module.exports = {
2
- preset: 'ts-jest',
3
- testEnvironment: 'node',
4
- roots: ['<rootDir>/tests'],
5
- testMatch: ['**/*.test.ts'],
6
- collectCoverageFrom: [
7
- 'src/**/*.ts',
8
- '!src/**/*.d.ts'
9
- ],
10
- coverageDirectory: 'coverage',
11
- verbose: true,
12
- moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
13
- transform: {
14
- '^.+\\.tsx?$': ['ts-jest', {
15
- tsconfig: 'tsconfig.test.json',
16
- diagnostics: false
17
- }]
18
- }
19
- };
package/saas-banner.svg DELETED
@@ -1,239 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 320" width="900" height="320">
2
- <defs>
3
- <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
4
- <stop offset="0%" style="stop-color:#0a0a0f"/>
5
- <stop offset="50%" style="stop-color:#0d1117"/>
6
- <stop offset="100%" style="stop-color:#111827"/>
7
- </linearGradient>
8
- <linearGradient id="pipeGrad" x1="0%" y1="0%" x2="100%" y2="0%">
9
- <stop offset="0%" style="stop-color:#26d97f;stop-opacity:0"/>
10
- <stop offset="50%" style="stop-color:#26d97f;stop-opacity:1"/>
11
- <stop offset="100%" style="stop-color:#26d97f;stop-opacity:0"/>
12
- </linearGradient>
13
- <linearGradient id="pipeGrad2" x1="0%" y1="0%" x2="100%" y2="0%">
14
- <stop offset="0%" style="stop-color:#3178c6;stop-opacity:0"/>
15
- <stop offset="50%" style="stop-color:#3178c6;stop-opacity:1"/>
16
- <stop offset="100%" style="stop-color:#3178c6;stop-opacity:0"/>
17
- </linearGradient>
18
- <linearGradient id="pipeGrad3" x1="0%" y1="0%" x2="100%" y2="0%">
19
- <stop offset="0%" style="stop-color:#ff9900;stop-opacity:0"/>
20
- <stop offset="50%" style="stop-color:#ff9900;stop-opacity:1"/>
21
- <stop offset="100%" style="stop-color:#ff9900;stop-opacity:0"/>
22
- </linearGradient>
23
- <filter id="glow-green">
24
- <feGaussianBlur stdDeviation="4" result="blur"/>
25
- <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
26
- </filter>
27
- <filter id="glow-blue">
28
- <feGaussianBlur stdDeviation="3" result="blur"/>
29
- <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
30
- </filter>
31
- <filter id="glow-soft">
32
- <feGaussianBlur stdDeviation="2" result="blur"/>
33
- <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
34
- </filter>
35
- </defs>
36
-
37
- <!-- Background -->
38
- <rect width="900" height="320" fill="url(#bg)" rx="14"/>
39
-
40
- <!-- Subtle dot-grid background -->
41
- <pattern id="dots" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">
42
- <circle cx="1" cy="1" r="0.8" fill="#1e293b" opacity="0.8"/>
43
- </pattern>
44
- <rect width="900" height="320" fill="url(#dots)" rx="14" opacity="0.6"/>
45
-
46
- <!-- ===================== DATA PIPELINE VISUALIZATION ===================== -->
47
- <!-- Horizontal pipeline backbone -->
48
- <line x1="60" y1="160" x2="840" y2="160" stroke="#1e293b" stroke-width="2"/>
49
-
50
- <!-- Animated data packet 1 - green -->
51
- <rect x="0" y="154" width="36" height="12" rx="6" fill="url(#pipeGrad)" opacity="0.9">
52
- <animate attributeName="x" values="-40;900" dur="3.2s" repeatCount="indefinite"/>
53
- </rect>
54
-
55
- <!-- Animated data packet 2 - blue -->
56
- <rect x="0" y="154" width="28" height="12" rx="6" fill="url(#pipeGrad2)" opacity="0.85">
57
- <animate attributeName="x" values="-40;900" dur="3.2s" begin="-1.1s" repeatCount="indefinite"/>
58
- </rect>
59
-
60
- <!-- Animated data packet 3 - orange -->
61
- <rect x="0" y="154" width="22" height="12" rx="6" fill="url(#pipeGrad3)" opacity="0.8">
62
- <animate attributeName="x" values="-40;900" dur="3.2s" begin="-2.0s" repeatCount="indefinite"/>
63
- </rect>
64
-
65
- <!-- ===================== MODULE NODES (pipeline stops) ===================== -->
66
- <!-- Each node represents a feature of the library -->
67
-
68
- <!-- Node 1: AUTH -->
69
- <g filter="url(#glow-blue)">
70
- <circle cx="130" cy="160" r="32" fill="#0d1117" stroke="#3178c6" stroke-width="1.8"/>
71
- <circle cx="130" cy="160" r="32" fill="none" stroke="#3178c6" stroke-width="1">
72
- <animate attributeName="r" values="32;40;32" dur="3s" repeatCount="indefinite"/>
73
- <animate attributeName="opacity" values="0.5;0;0.5" dur="3s" repeatCount="indefinite"/>
74
- </circle>
75
- <text x="130" y="155" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#3178c6" font-weight="bold">🔐</text>
76
- <text x="130" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#3178c6" font-weight="bold">AUTH</text>
77
- <text x="130" y="204" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#3178c6" opacity="0.6">JWT · RBAC</text>
78
- </g>
79
-
80
- <!-- Connector line -->
81
- <line x1="162" y1="160" x2="218" y2="160" stroke="#1e293b" stroke-width="1.5" stroke-dasharray="4,3"/>
82
-
83
- <!-- Node 2: QUEUE -->
84
- <g filter="url(#glow-green)">
85
- <circle cx="250" cy="160" r="32" fill="#0d1117" stroke="#26d97f" stroke-width="1.8"/>
86
- <circle cx="250" cy="160" r="32" fill="none" stroke="#26d97f" stroke-width="1">
87
- <animate attributeName="r" values="32;40;32" dur="3.5s" begin="-0.8s" repeatCount="indefinite"/>
88
- <animate attributeName="opacity" values="0.5;0;0.5" dur="3.5s" begin="-0.8s" repeatCount="indefinite"/>
89
- </circle>
90
- <text x="250" y="155" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#26d97f" font-weight="bold">⚡</text>
91
- <text x="250" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#26d97f" font-weight="bold">QUEUE</text>
92
- <text x="250" y="204" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#26d97f" opacity="0.6">BullMQ</text>
93
- </g>
94
-
95
- <!-- Connector line -->
96
- <line x1="282" y1="160" x2="338" y2="160" stroke="#1e293b" stroke-width="1.5" stroke-dasharray="4,3"/>
97
-
98
- <!-- Node 3: NOTIFY -->
99
- <g filter="url(#glow-soft)">
100
- <circle cx="370" cy="160" r="32" fill="#0d1117" stroke="#f59e0b" stroke-width="1.8"/>
101
- <circle cx="370" cy="160" r="32" fill="none" stroke="#f59e0b" stroke-width="1">
102
- <animate attributeName="r" values="32;40;32" dur="4s" begin="-1.5s" repeatCount="indefinite"/>
103
- <animate attributeName="opacity" values="0.5;0;0.5" dur="4s" begin="-1.5s" repeatCount="indefinite"/>
104
- </circle>
105
- <text x="370" y="155" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#f59e0b" font-weight="bold">🔔</text>
106
- <text x="370" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#f59e0b" font-weight="bold">NOTIFY</text>
107
- <text x="370" y="204" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#f59e0b" opacity="0.6">Email · SMS</text>
108
- </g>
109
-
110
- <!-- Connector line -->
111
- <line x1="402" y1="160" x2="458" y2="160" stroke="#1e293b" stroke-width="1.5" stroke-dasharray="4,3"/>
112
-
113
- <!-- Node 4: UPLOAD (CENTER - largest) -->
114
- <g filter="url(#glow-soft)">
115
- <circle cx="490" cy="160" r="36" fill="#0d1117" stroke="#ff9900" stroke-width="2"/>
116
- <circle cx="490" cy="160" r="36" fill="none" stroke="#ff9900" stroke-width="1.2">
117
- <animate attributeName="r" values="36;48;36" dur="3s" begin="-0.4s" repeatCount="indefinite"/>
118
- <animate attributeName="opacity" values="0.5;0;0.5" dur="3s" begin="-0.4s" repeatCount="indefinite"/>
119
- </circle>
120
- <text x="490" y="154" text-anchor="middle" font-family="'Courier New',monospace" font-size="10" fill="#ff9900" font-weight="bold">☁️</text>
121
- <text x="490" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="10" fill="#ff9900" font-weight="bold">S3</text>
122
- <text x="490" y="208" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#ff9900" opacity="0.6">AWS Upload</text>
123
- </g>
124
-
125
- <!-- Connector line -->
126
- <line x1="526" y1="160" x2="578" y2="160" stroke="#1e293b" stroke-width="1.5" stroke-dasharray="4,3"/>
127
-
128
- <!-- Node 5: RATE LIMIT -->
129
- <g filter="url(#glow-soft)">
130
- <circle cx="610" cy="160" r="32" fill="#0d1117" stroke="#e11d48" stroke-width="1.8"/>
131
- <circle cx="610" cy="160" r="32" fill="none" stroke="#e11d48" stroke-width="1">
132
- <animate attributeName="r" values="32;40;32" dur="2.8s" begin="-2.1s" repeatCount="indefinite"/>
133
- <animate attributeName="opacity" values="0.5;0;0.5" dur="2.8s" begin="-2.1s" repeatCount="indefinite"/>
134
- </circle>
135
- <text x="610" y="155" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#e11d48" font-weight="bold">🛡️</text>
136
- <text x="610" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#e11d48" font-weight="bold">LIMIT</text>
137
- <text x="610" y="204" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#e11d48" opacity="0.6">Rate Guard</text>
138
- </g>
139
-
140
- <!-- Connector line -->
141
- <line x1="642" y1="160" x2="698" y2="160" stroke="#1e293b" stroke-width="1.5" stroke-dasharray="4,3"/>
142
-
143
- <!-- Node 6: LOGGER -->
144
- <g filter="url(#glow-soft)">
145
- <circle cx="730" cy="160" r="32" fill="#0d1117" stroke="#8b5cf6" stroke-width="1.8"/>
146
- <circle cx="730" cy="160" r="32" fill="none" stroke="#8b5cf6" stroke-width="1">
147
- <animate attributeName="r" values="32;40;32" dur="3.8s" begin="-1.0s" repeatCount="indefinite"/>
148
- <animate attributeName="opacity" values="0.5;0;0.5" dur="3.8s" begin="-1.0s" repeatCount="indefinite"/>
149
- </circle>
150
- <text x="730" y="155" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#8b5cf6" font-weight="bold">📋</text>
151
- <text x="730" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#8b5cf6" font-weight="bold">LOGGER</text>
152
- <text x="730" y="204" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#8b5cf6" opacity="0.6">Pino · JSON</text>
153
- </g>
154
-
155
- <!-- Connector line -->
156
- <line x1="762" y1="160" x2="818" y2="160" stroke="#1e293b" stroke-width="1.5" stroke-dasharray="4,3"/>
157
-
158
- <!-- Node 7: CONFIG -->
159
- <g filter="url(#glow-soft)">
160
- <circle cx="845" cy="160" r="28" fill="#0d1117" stroke="#06b6d4" stroke-width="1.8"/>
161
- <circle cx="845" cy="160" r="28" fill="none" stroke="#06b6d4" stroke-width="1">
162
- <animate attributeName="r" values="28;36;28" dur="3.2s" begin="-2.6s" repeatCount="indefinite"/>
163
- <animate attributeName="opacity" values="0.5;0;0.5" dur="3.2s" begin="-2.6s" repeatCount="indefinite"/>
164
- </circle>
165
- <text x="845" y="155" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#06b6d4" font-weight="bold">⚙️</text>
166
- <text x="845" y="168" text-anchor="middle" font-family="'Courier New',monospace" font-size="9" fill="#06b6d4" font-weight="bold">CFG</text>
167
- <text x="845" y="204" text-anchor="middle" font-family="'Courier New',monospace" font-size="8" fill="#06b6d4" opacity="0.6">Zod · Env</text>
168
- </g>
169
-
170
- <!-- ===================== TITLE + SUBTITLE ===================== -->
171
- <text x="450" y="50" text-anchor="middle" font-family="'Courier New',monospace" font-size="28" font-weight="bold" fill="#f8fafc" letter-spacing="2" filter="url(#glow-soft)">
172
- saas-backend-kit
173
- <animate attributeName="opacity" values="0.85;1;0.85" dur="4s" repeatCount="indefinite"/>
174
- </text>
175
-
176
- <text x="450" y="76" text-anchor="middle" font-family="'Courier New',monospace" font-size="12" fill="#64748b">
177
- Production-grade modular backend toolkit · Node.js · Express · Fastify
178
- </text>
179
-
180
- <!-- npm install command badge — animated border -->
181
- <rect x="310" y="88" width="280" height="30" rx="6" fill="#0d1117" stroke="#26d97f" stroke-width="1" opacity="0.9">
182
- <animate attributeName="stroke-opacity" values="0.4;1;0.4" dur="2s" repeatCount="indefinite"/>
183
- </rect>
184
- <text x="450" y="108" text-anchor="middle" font-family="'Courier New',monospace" font-size="12" fill="#26d97f">
185
- $ npm install saas-backend-kit
186
- </text>
187
-
188
- <!-- ===================== FLOATING PARTICLES ===================== -->
189
- <circle cx="50" cy="50" r="2" fill="#26d97f" opacity="0.5">
190
- <animate attributeName="cy" values="50;270;50" dur="7s" repeatCount="indefinite"/>
191
- <animate attributeName="opacity" values="0.5;0;0.5" dur="7s" repeatCount="indefinite"/>
192
- </circle>
193
- <circle cx="870" cy="90" r="1.5" fill="#3178c6" opacity="0.5">
194
- <animate attributeName="cy" values="90;30;90" dur="5s" repeatCount="indefinite"/>
195
- <animate attributeName="opacity" values="0.5;0;0.5" dur="5s" repeatCount="indefinite"/>
196
- </circle>
197
- <circle cx="200" cy="280" r="2" fill="#f59e0b" opacity="0.4">
198
- <animate attributeName="cx" values="200;700;200" dur="11s" repeatCount="indefinite"/>
199
- <animate attributeName="opacity" values="0.4;0;0.4" dur="11s" repeatCount="indefinite"/>
200
- </circle>
201
- <circle cx="750" cy="30" r="1.5" fill="#8b5cf6" opacity="0.45">
202
- <animate attributeName="cx" values="750;200;750" dur="9s" repeatCount="indefinite"/>
203
- <animate attributeName="opacity" values="0.45;0;0.45" dur="9s" repeatCount="indefinite"/>
204
- </circle>
205
-
206
- <!-- ===================== BOTTOM STATUS BAR ===================== -->
207
- <rect x="0" y="285" width="900" height="35" fill="#080c10" rx="0"/>
208
- <rect x="0" y="282" width="900" height="3" fill="#26d97f" opacity="0.25"/>
209
-
210
- <!-- Status indicators -->
211
- <circle cx="30" cy="302" r="5" fill="#26d97f">
212
- <animate attributeName="opacity" values="1;0.3;1" dur="1.5s" repeatCount="indefinite"/>
213
- </circle>
214
- <text x="42" y="307" font-family="'Courier New',monospace" font-size="10" fill="#26d97f" opacity="0.8">LIVE</text>
215
-
216
- <text x="90" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b">|</text>
217
- <text x="100" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b" opacity="0.7">MIT Licensed</text>
218
-
219
- <text x="210" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b">|</text>
220
- <text x="220" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b" opacity="0.7">TypeScript Ready</text>
221
-
222
- <text x="360" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b">|</text>
223
- <text x="370" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b" opacity="0.7">7 Modular Plugins</text>
224
-
225
- <text x="520" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b">|</text>
226
- <text x="530" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b" opacity="0.7">Express · Fastify</text>
227
-
228
- <text x="670" y="307" font-family="'Courier New',monospace" font-size="10" fill="#64748b">|</text>
229
- <text x="680" y="307" font-family="'Courier New',monospace" font-size="10" fill="#3178c6" opacity="0.85">by @AshishK-M</text>
230
-
231
- <!-- Scrolling ticker on right -->
232
- <clipPath id="tickerClip">
233
- <rect x="800" y="288" width="95" height="28"/>
234
- </clipPath>
235
- <text font-family="'Courier New',monospace" font-size="9" fill="#26d97f" opacity="0.6" clip-path="url(#tickerClip)">
236
- <tspan x="900" y="305">auth · queue · notify · upload · logger · rateLimit · config</tspan>
237
- <animate attributeName="x" values="900;600" dur="8s" repeatCount="indefinite"/>
238
- </text>
239
- </svg>