From 608912a945f18ce97a687a44bb3982197f62e2d3 Mon Sep 17 00:00:00 2001 From: Isaac Muniz Date: Wed, 10 Dec 2025 16:30:24 -0300 Subject: [PATCH 01/10] docs: create the project's `README.md` --- README.md | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index df53f77..1f6dd21 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,138 @@ -# tabnews-clone +# SaaS-Base -Implementação do tabnews.com.br para o curso.dev. +## Description + +A nice starting point for SaaS projects. + +## Table of Contents + +- [Description](#description) +- [Table of Contents](#table-of-contents) +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation](#installation) +- [Usage](#usage) +- [Development](#development) + - [Scripts](#scripts) +- [Testing](#testing) +- [Contributing](#contributing) +- [License](#license) + +## Getting Started + +This section will guide you through setting up the project locally. + +### Prerequisites + +Before you begin, ensure you have the following installed: + +- Node.js (version specified in `.nvmrc`): `18` +- Docker and Docker Compose + +### Installation + +1. Clone the repository: + + ```bash + git clone https://github.com/codigoisaac/SaaS-Base.git + cd SaaS-Base + ``` + +2. Install dependencies: + + ```bash + npm install + ``` + +3. Set up the environment: + - Copy `.env.development` and modify the variables as needed. + +4. Start the services using Docker Compose: + + ```bash + npm run services:up + ``` + +## Usage + +Explain how to use the application. Include examples and use cases. + +## Development + +### Scripts + +The following scripts are available for development: + +- `dev`: Starts the development server, database, and runs migrations. + ```bash + npm run dev + ``` +- `test`: Runs the test suite. + ```bash + npm run test + ``` +- `test:watch`: Runs the test suite in watch mode. + ```bash + npm run test:watch + ``` +- `services:up`: Starts the Docker services. + ```bash + npm run services:up + ``` +- `services:stop`: Stops the Docker services. + ```bash + npm run services:stop + ``` +- `services:down`: Stops and removes the Docker containers. + ```bash + npm run services:down + ``` +- `services:wait:database`: Waits for the PostgreSQL database to be ready. + ```bash + npm run services:wait:database + ``` +- `migrations:create`: Creates a new migration file. + ```bash + npm run migrations:create + ``` +- `migrations:up`: Runs the database migrations. + ```bash + npm run migrations:up + ``` +- `lint:prettier:check`: Checks code formatting with Prettier. + ```bash + npm run lint:prettier:check + ``` +- `lint:prettier:fix`: Fixes code formatting with Prettier. + ```bash + npm run lint:prettier:fix + ``` +- `lint:eslint:check`: Checks for ESLint issues. + ```bash + npm run lint:eslint:check + ``` +- `commit`: Uses commitizen to create conventional commits. + ```bash + npm run commit + ``` + +## Testing + +To run the tests, use the following command: + +```bash +npm test +``` + +## Contributing + +Contributions are welcome! Please follow these steps: + +1. Fork the repository. +2. Create a new branch for your feature or bug fix. +3. Make your changes and commit them using conventional commits. +4. Submit a pull request. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. From 1be3c2900a48485cb8559ae4b403482529c03f89 Mon Sep 17 00:00:00 2001 From: Isaac Muniz Date: Wed, 10 Dec 2025 22:45:59 -0300 Subject: [PATCH 02/10] feat: adds a list of the project's features in the main page --- pages/index.js | 129 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/pages/index.js b/pages/index.js index 922847f..425b1fa 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,5 +1,132 @@ function Home() { - return

Em construção. 🏗️

; + return ( + <> +

SaaS Base

+ +

+ It is much easier to build your SaaS from here, as this is made to cover + the foundational stuff well, and to be extensible 😉 +

+ +

It comes with:

+ + + + ); } export default Home; From dd150f843bd5271751f866af4f64e1c59e5602d5 Mon Sep 17 00:00:00 2001 From: Isaac Muniz Date: Wed, 10 Dec 2025 23:05:18 -0300 Subject: [PATCH 03/10] feat: add simple responsive style for better readability and add a footer --- pages/index.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/pages/index.js b/pages/index.js index 425b1fa..3cd3ddb 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,6 +1,13 @@ function Home() { return ( - <> +

SaaS Base

@@ -125,7 +132,21 @@ function Home() {
  • And other stuff that I may have forgotten to include here.
  • - + + + Please, help your fellow developer to ascend in its carreer and{" "} + + give the project a star on GitHub + {" "} + 🍻 + + +
    +
    +
    + + Thank you for using SaaS Base. +

    ); } From 7801a8fcdcaf2413965ebf35d1b9a9350d2118ca Mon Sep 17 00:00:00 2001 From: Isaac Muniz Date: Thu, 11 Dec 2025 23:10:25 -0300 Subject: [PATCH 04/10] refactor: improve code clarity in `wait-for-postgres.js` --- infra/scripts/wait-for-postgres.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/infra/scripts/wait-for-postgres.js b/infra/scripts/wait-for-postgres.js index b3b9d06..1a27b27 100644 --- a/infra/scripts/wait-for-postgres.js +++ b/infra/scripts/wait-for-postgres.js @@ -4,7 +4,9 @@ function checkPostgres() { exec("docker exec postgres-dev pg_isready --host localhost", handleReturn); function handleReturn(error, stdout) { - if (stdout.search("accepting connections") === -1) { + const postgresIsReady = stdout.search("accepting connections") !== -1; + + if (!postgresIsReady) { process.stdout.write("."); checkPostgres(); return; @@ -15,4 +17,5 @@ function checkPostgres() { } process.stdout.write("\n\n🔴 Waiting for Postgres to accept connections..."); + checkPostgres(); From 02a5c9838464e3d1ab5d2a7e76da4962905af576 Mon Sep 17 00:00:00 2001 From: Isaac Muniz Date: Thu, 11 Dec 2025 23:11:11 -0300 Subject: [PATCH 05/10] docs: improve readme with a better project description --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1f6dd21..39b2437 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,19 @@ # SaaS-Base -## Description +An elegant foundation for your next SaaS project, featuring: -A nice starting point for SaaS projects. +- A unified `npm run dev` workflow that: + - Starts the project's Docker stack (currently just the Postgres development database defined in `/infra/compose.yaml`). + - Waits for Postgres to become ready using the `/infra/scripts/wait-for-postgres.js` helper. + - Runs all pending database migrations once the connection is available. + - Boots the Next.js development server only after the database is fully prepared. + +- A unified `npm run test` (or simply `npm test`) workflow that: + - Starts the project's Docker stack (currently just the Postgres development database defined in `/infra/compose.yaml`). + - Waits for Postgres to become ready using the `/infra/scripts/wait-for-postgres.js` helper. + - Uses the `concurrently` package to run the Next.js development server and the Jest test runner in parallel, executing the project's automated tests. + +- A `posttest` script that will run automatically after the `test` script above, and that will stop all Docker services using the command `services:stop ## Table of Contents From a02acfadf552941d50e69c2a9e24913142fcde31 Mon Sep 17 00:00:00 2001 From: Isaac Muniz Date: Fri, 12 Dec 2025 23:21:18 -0300 Subject: [PATCH 06/10] build(dependencies): upgrade next.js and node-pg-migrate Next.js bacuse of critical secutity issues and node-pg-migrate so we can use ES Modules in the project. --- package-lock.json | 215 +++++++++++++++++++++++++++++++--------------- package.json | 5 +- 2 files changed, 149 insertions(+), 71 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3284c4b..8daa40f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,8 @@ "async-retry": "1.3.3", "dotenv": "17.2.3", "dotenv-expand": "12.0.3", - "next": "16.0.8", - "node-pg-migrate": "7.6.1", + "next": "16.0.10", + "node-pg-migrate": "^8.0.4", "pg": "8.16.3", "react": "19.2.1", "react-dom": "19.2.1" @@ -1654,11 +1654,31 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -1676,7 +1696,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1689,7 +1708,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1702,14 +1720,12 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -1727,7 +1743,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -1743,7 +1758,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -2368,9 +2382,9 @@ } }, "node_modules/@next/env": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.8.tgz", - "integrity": "sha512-xP4WrQZuj9MdmLJy3eWFHepo+R3vznsMSS8Dy3wdA7FKpjCiesQ6DxZvdGziQisj0tEtCgBKJzjcAc4yZOgLEQ==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz", + "integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -2414,9 +2428,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.8.tgz", - "integrity": "sha512-yjVMvTQN21ZHOclQnhSFbjBTEizle+1uo4NV6L4rtS9WO3nfjaeJYw+H91G+nEf3Ef43TaEZvY5mPWfB/De7tA==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.10.tgz", + "integrity": "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==", "cpu": [ "arm64" ], @@ -2430,9 +2444,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.8.tgz", - "integrity": "sha512-+zu2N3QQ0ZOb6RyqQKfcu/pn0UPGmg+mUDqpAAEviAcEVEYgDckemOpiMRsBP3IsEKpcoKuNzekDcPczEeEIzA==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.10.tgz", + "integrity": "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==", "cpu": [ "x64" ], @@ -2446,9 +2460,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.8.tgz", - "integrity": "sha512-LConttk+BeD0e6RG0jGEP9GfvdaBVMYsLJ5aDDweKiJVVCu6sGvo+Ohz9nQhvj7EQDVVRJMCGhl19DmJwGr6bQ==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.10.tgz", + "integrity": "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==", "cpu": [ "arm64" ], @@ -2462,9 +2476,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.8.tgz", - "integrity": "sha512-JaXFAlqn8fJV+GhhA9lpg6da/NCN/v9ub98n3HoayoUSPOVdoxEEt86iT58jXqQCs/R3dv5ZnxGkW8aF4obMrQ==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.10.tgz", + "integrity": "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==", "cpu": [ "arm64" ], @@ -2478,9 +2492,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.8.tgz", - "integrity": "sha512-O7M9it6HyNhsJp3HNAsJoHk5BUsfj7hRshfptpGcVsPZ1u0KQ/oVy8oxF7tlwxA5tR43VUP0yRmAGm1us514ng==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.10.tgz", + "integrity": "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==", "cpu": [ "x64" ], @@ -2494,9 +2508,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.8.tgz", - "integrity": "sha512-8+KClEC/GLI2dLYcrWwHu5JyC5cZYCFnccVIvmxpo6K+XQt4qzqM5L4coofNDZYkct/VCCyJWGbZZDsg6w6LFA==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.10.tgz", + "integrity": "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==", "cpu": [ "x64" ], @@ -2510,9 +2524,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.8.tgz", - "integrity": "sha512-rpQ/PgTEgH68SiXmhu/cJ2hk9aZ6YgFvspzQWe2I9HufY6g7V02DXRr/xrVqOaKm2lenBFPNQ+KAaeveywqV+A==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.10.tgz", + "integrity": "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==", "cpu": [ "arm64" ], @@ -2526,9 +2540,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.8.tgz", - "integrity": "sha512-jWpWjWcMQu2iZz4pEK2IktcfR+OA9+cCG8zenyLpcW8rN4rzjfOzH4yj/b1FiEAZHKS+5Vq8+bZyHi+2yqHbFA==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz", + "integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==", "cpu": [ "x64" ], @@ -4577,7 +4591,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -4984,7 +4997,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -6083,7 +6095,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -6100,7 +6111,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -7349,7 +7359,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -7454,6 +7463,21 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", @@ -8665,7 +8689,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -8727,12 +8750,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/next/-/next-16.0.8.tgz", - "integrity": "sha512-LmcZzG04JuzNXi48s5P+TnJBsTGPJunViNKV/iE4uM6kstjTQsQhvsAv+xF6MJxU2Pr26tl15eVbp0jQnsv6/g==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz", + "integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==", "license": "MIT", "dependencies": { - "@next/env": "16.0.8", + "@next/env": "16.0.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -8745,14 +8768,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.0.8", - "@next/swc-darwin-x64": "16.0.8", - "@next/swc-linux-arm64-gnu": "16.0.8", - "@next/swc-linux-arm64-musl": "16.0.8", - "@next/swc-linux-x64-gnu": "16.0.8", - "@next/swc-linux-x64-musl": "16.0.8", - "@next/swc-win32-arm64-msvc": "16.0.8", - "@next/swc-win32-x64-msvc": "16.0.8", + "@next/swc-darwin-arm64": "16.0.10", + "@next/swc-darwin-x64": "16.0.10", + "@next/swc-linux-arm64-gnu": "16.0.10", + "@next/swc-linux-arm64-musl": "16.0.10", + "@next/swc-linux-x64-gnu": "16.0.10", + "@next/swc-linux-x64-musl": "16.0.10", + "@next/swc-win32-arm64-msvc": "16.0.10", + "@next/swc-win32-x64-msvc": "16.0.10", "sharp": "^0.34.4" }, "peerDependencies": { @@ -8786,20 +8809,19 @@ "license": "MIT" }, "node_modules/node-pg-migrate": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/node-pg-migrate/-/node-pg-migrate-7.6.1.tgz", - "integrity": "sha512-CUocdo8kh25QZU2MeCjss2/OQ/Jq8ejCeilpja1MVOgk2c03r4U7vux9fAZCfrVg0YASwnhnpjz+rSK4tAVB2w==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/node-pg-migrate/-/node-pg-migrate-8.0.4.tgz", + "integrity": "sha512-HTlJ6fOT/2xHhAUtsqSN85PGMAqSbfGJNRwQF8+ZwQ1+sVGNUTl/ZGEshPsOI3yV22tPIyHXrKXr3S0JxeYLrg==", "license": "MIT", "dependencies": { + "glob": "~11.1.0", "yargs": "~17.7.0" }, "bin": { - "node-pg-migrate": "bin/node-pg-migrate.js", - "node-pg-migrate-cjs": "bin/node-pg-migrate.js", - "node-pg-migrate-esm": "bin/node-pg-migrate.mjs" + "node-pg-migrate": "bin/node-pg-migrate.js" }, "engines": { - "node": ">=18.19.0" + "node": ">=20.11.0" }, "peerDependencies": { "@types/pg": ">=6.0.0 <9.0.0", @@ -8811,6 +8833,69 @@ } } }, + "node_modules/node-pg-migrate/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-pg-migrate/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/node-pg-migrate/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-pg-migrate/node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -9119,7 +9204,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -9188,7 +9272,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10027,7 +10110,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -10040,7 +10122,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10275,7 +10356,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -10416,7 +10496,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11038,7 +11117,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -11171,7 +11249,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", diff --git a/package.json b/package.json index 52d756c..a7f8520 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "license": "MIT", "author": "Isaac Muniz", "main": "index.js", + "type": "module", "scripts": { "dev": "npm run services:up && npm run services:wait:database && npm run migrations:up && next dev", "test": "npm run services:up && npm run services:wait:database && concurrently --names next,jest --hide next --kill-others --success command-jest 'next dev' 'jest --runInBand --verbose'", @@ -34,8 +35,8 @@ "async-retry": "1.3.3", "dotenv": "17.2.3", "dotenv-expand": "12.0.3", - "next": "16.0.8", - "node-pg-migrate": "7.6.1", + "next": "16.0.10", + "node-pg-migrate": "8.0.4", "pg": "8.16.3", "react": "19.2.1", "react-dom": "19.2.1" From 021428b437ecc67d094bb0287936d1680a67a857 Mon Sep 17 00:00:00 2001 From: Isaac Muniz Date: Fri, 12 Dec 2025 23:30:01 -0300 Subject: [PATCH 07/10] chore: change project to use ES Modules --- commitlint.config.js | 4 +++- .../migrations/1764719520015_test-migration.js | 7 ------- .../migrations/1765591892999_test-migration.js | 18 ++++++++++++++++++ infra/scripts/orchestrator.js | 2 +- infra/scripts/wait-for-postgres.js | 2 +- jest.config.js | 8 ++++---- 6 files changed, 27 insertions(+), 14 deletions(-) delete mode 100644 infra/migrations/1764719520015_test-migration.js create mode 100644 infra/migrations/1765591892999_test-migration.js diff --git a/commitlint.config.js b/commitlint.config.js index 69b4242..0374f70 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,3 +1,5 @@ -module.exports = { +const commitlintConfig = { extends: ["@commitlint/config-conventional"], }; + +export default commitlintConfig; diff --git a/infra/migrations/1764719520015_test-migration.js b/infra/migrations/1764719520015_test-migration.js deleted file mode 100644 index 086f59e..0000000 --- a/infra/migrations/1764719520015_test-migration.js +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-unused-vars */ - -exports.shorthands = undefined; - -exports.up = (pgm) => {}; - -exports.down = (pgm) => {}; diff --git a/infra/migrations/1765591892999_test-migration.js b/infra/migrations/1765591892999_test-migration.js new file mode 100644 index 0000000..97e8e32 --- /dev/null +++ b/infra/migrations/1765591892999_test-migration.js @@ -0,0 +1,18 @@ +/** + * @type {import('node-pg-migrate').ColumnDefinitions | undefined} + */ +export const shorthands = undefined; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const up = (pgm) => {}; + +/** + * @param pgm {import('node-pg-migrate').MigrationBuilder} + * @param run {() => void | undefined} + * @returns {Promise | void} + */ +export const down = (pgm) => {}; diff --git a/infra/scripts/orchestrator.js b/infra/scripts/orchestrator.js index 5198282..8203faa 100644 --- a/infra/scripts/orchestrator.js +++ b/infra/scripts/orchestrator.js @@ -1,5 +1,5 @@ import retry from "async-retry"; -import database from "infra/database"; +import database from "../database"; async function waitForAllServices() { await waitForWebServer(); diff --git a/infra/scripts/wait-for-postgres.js b/infra/scripts/wait-for-postgres.js index 1a27b27..5327960 100644 --- a/infra/scripts/wait-for-postgres.js +++ b/infra/scripts/wait-for-postgres.js @@ -1,4 +1,4 @@ -const { exec } = require("node:child_process"); +import { exec } from "node:child_process"; function checkPostgres() { exec("docker exec postgres-dev pg_isready --host localhost", handleReturn); diff --git a/jest.config.js b/jest.config.js index 92818c6..170149d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,7 @@ -const dotenv = require("dotenv"); -dotenv.config({ path: ".env.development" }); +import dotenv from "dotenv"; +import nextJest from "next/jest.js"; -const nextJest = require("next/jest"); +dotenv.config({ path: ".env.development" }); const createJestConfig = nextJest({ dir: ".", @@ -12,4 +12,4 @@ const jestConfig = createJestConfig({ testTimeout: 60000, }); -module.exports = jestConfig; +export default jestConfig; From 7c726095beef0e01fcce83663fd6ffe51d07feda Mon Sep 17 00:00:00 2001 From: Isaac Muniz Date: Sat, 13 Dec 2025 01:33:54 -0300 Subject: [PATCH 08/10] fix: separate dev tests from ci tests Next.js 16 no longer accepts multiple instances running at the same time, so we couldn't start a second server for tests. Now in development environment we need to have Next.js server already running in order to run the tests. --- .github/workflows/tests.yaml | 2 +- infra/scripts/orchestrator.js | 2 ++ jest.config.js | 2 +- package.json | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 6522cba..08abbc9 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -15,4 +15,4 @@ jobs: - run: npm ci - - run: npm test + - run: npm run test:ci diff --git a/infra/scripts/orchestrator.js b/infra/scripts/orchestrator.js index 8203faa..0402da3 100644 --- a/infra/scripts/orchestrator.js +++ b/infra/scripts/orchestrator.js @@ -1,6 +1,8 @@ import retry from "async-retry"; import database from "../database"; +// Web server must be running in order for the tests to work. Otherwise they will take some time and then timeout. Make sure you ran `npm run dev` on another terminal. + async function waitForAllServices() { await waitForWebServer(); diff --git a/jest.config.js b/jest.config.js index 170149d..463b5de 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,7 +9,7 @@ const createJestConfig = nextJest({ const jestConfig = createJestConfig({ moduleDirectories: ["node_modules", ""], - testTimeout: 60000, + testTimeout: 15000, }); export default jestConfig; diff --git a/package.json b/package.json index a7f8520..d554d89 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "type": "module", "scripts": { "dev": "npm run services:up && npm run services:wait:database && npm run migrations:up && next dev", - "test": "npm run services:up && npm run services:wait:database && concurrently --names next,jest --hide next --kill-others --success command-jest 'next dev' 'jest --runInBand --verbose'", - "posttest": "npm run services:stop", + "test": "jest --runInBand --verbose", + "test:ci": "npm run services:up && npm run services:wait:database && concurrently --names next,jest --kill-others --success command-jest 'next dev' 'jest --runInBand --verbose'", "test:watch": "jest --watchAll --runInBand --verbose", "services:up": "docker compose -f infra/compose.yaml up -d", "services:stop": "docker compose -f infra/compose.yaml stop", From 90b685ace71734eb6b0a2f90964e05670b5ca4eb Mon Sep 17 00:00:00 2001 From: Isaac Muniz Date: Sat, 13 Dec 2025 01:41:21 -0300 Subject: [PATCH 09/10] fix: migrations file linting --- infra/migrations/1765591892999_test-migration.js | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/migrations/1765591892999_test-migration.js b/infra/migrations/1765591892999_test-migration.js index 97e8e32..652497b 100644 --- a/infra/migrations/1765591892999_test-migration.js +++ b/infra/migrations/1765591892999_test-migration.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-vars */ /** * @type {import('node-pg-migrate').ColumnDefinitions | undefined} */ From f41f9c232ead9f7537ee1cbcb7ddb2b48a50935a Mon Sep 17 00:00:00 2001 From: Isaac Muniz Date: Sat, 13 Dec 2025 03:00:57 -0300 Subject: [PATCH 10/10] docs: add a proper `README` --- README.md | 247 +++++++++++++++++++++---------------------------- image.png | Bin 0 -> 49114 bytes pages/index.js | 134 +-------------------------- 3 files changed, 113 insertions(+), 268 deletions(-) create mode 100644 image.png diff --git a/README.md b/README.md index 39b2437..ded68fb 100644 --- a/README.md +++ b/README.md @@ -1,149 +1,118 @@ # SaaS-Base -An elegant foundation for your next SaaS project, featuring: - -- A unified `npm run dev` workflow that: - - Starts the project's Docker stack (currently just the Postgres development database defined in `/infra/compose.yaml`). - - Waits for Postgres to become ready using the `/infra/scripts/wait-for-postgres.js` helper. - - Runs all pending database migrations once the connection is available. - - Boots the Next.js development server only after the database is fully prepared. - -- A unified `npm run test` (or simply `npm test`) workflow that: - - Starts the project's Docker stack (currently just the Postgres development database defined in `/infra/compose.yaml`). - - Waits for Postgres to become ready using the `/infra/scripts/wait-for-postgres.js` helper. - - Uses the `concurrently` package to run the Next.js development server and the Jest test runner in parallel, executing the project's automated tests. - -- A `posttest` script that will run automatically after the `test` script above, and that will stop all Docker services using the command `services:stop - -## Table of Contents - -- [Description](#description) -- [Table of Contents](#table-of-contents) -- [Getting Started](#getting-started) - - [Prerequisites](#prerequisites) - - [Installation](#installation) -- [Usage](#usage) -- [Development](#development) - - [Scripts](#scripts) -- [Testing](#testing) -- [Contributing](#contributing) -- [License](#license) - -## Getting Started - -This section will guide you through setting up the project locally. - -### Prerequisites - -Before you begin, ensure you have the following installed: - -- Node.js (version specified in `.nvmrc`): `18` -- Docker and Docker Compose - -### Installation - -1. Clone the repository: - - ```bash - git clone https://github.com/codigoisaac/SaaS-Base.git - cd SaaS-Base - ``` - -2. Install dependencies: - - ```bash - npm install - ``` - -3. Set up the environment: - - Copy `.env.development` and modify the variables as needed. - -4. Start the services using Docker Compose: - - ```bash - npm run services:up - ``` - -## Usage - -Explain how to use the application. Include examples and use cases. - -## Development - -### Scripts - -The following scripts are available for development: - -- `dev`: Starts the development server, database, and runs migrations. - ```bash - npm run dev - ``` -- `test`: Runs the test suite. - ```bash - npm run test - ``` -- `test:watch`: Runs the test suite in watch mode. - ```bash - npm run test:watch - ``` -- `services:up`: Starts the Docker services. - ```bash - npm run services:up - ``` -- `services:stop`: Stops the Docker services. - ```bash - npm run services:stop - ``` -- `services:down`: Stops and removes the Docker containers. - ```bash - npm run services:down - ``` -- `services:wait:database`: Waits for the PostgreSQL database to be ready. - ```bash - npm run services:wait:database - ``` -- `migrations:create`: Creates a new migration file. - ```bash - npm run migrations:create - ``` -- `migrations:up`: Runs the database migrations. - ```bash - npm run migrations:up - ``` -- `lint:prettier:check`: Checks code formatting with Prettier. - ```bash - npm run lint:prettier:check - ``` -- `lint:prettier:fix`: Fixes code formatting with Prettier. - ```bash - npm run lint:prettier:fix - ``` -- `lint:eslint:check`: Checks for ESLint issues. - ```bash - npm run lint:eslint:check - ``` -- `commit`: Uses commitizen to create conventional commits. - ```bash - npm run commit - ``` - -## Testing - -To run the tests, use the following command: +A **lean, fully wired foundation for SaaS development**. It provides a complete Next.js setup, **Docker-managed PostgreSQL**, structured **migrations**, and automated environment orchestration - so your project boots with a single command and stays consistent across machines. + +The foundation also includes **integration testing**, plus enforced code quality through **ESLint** and **Prettier**. For Git hygiene, it uses **Husky**, **Commitlint**, and **Commitizen** to keep your commit history clean, predictable, and automation-friendly. + +Everything is preconfigured to eliminate setup friction, prevent “works on my machine” headaches, and let you focus directly on building product features - not infrastructure. + +--- + +## 🚀 Features + +### 🌟 Automated Dev Workflow + +The automated development workflow ensures you never waste time manually preparing your environment. + +Instead of juggling multiple terminal commands, connection waits, and database setup steps, a single script handles everything: starting infrastructure, validating service readiness, applying migrations, and booting the application. This removes entire classes of “it works on my machine” problems, keeps onboarding effortless for new developers, and makes day-to-day development dramatically smoother and more reliable. + +The `dev` script performs everything in sequence: + +1. Starts Docker services +2. Waits for PostgreSQL +3. Applies migrations +4. Runs Next.js in development mode + +Run it with: ```bash -npm test +npm run dev ``` -## Contributing +Under the hood, what this command will do is: + +``` +npm run services:up && npm run services:wait:database && npm run migrations:up && next dev +``` + +### 🌟 Husky + Commitlint + Commitizen + +To ensure your project’s Git history stays clean, consistent, and reliable. + +Husky enforces checks before bad commits sneak in, Commitlint guarantees every commit message follows a predictable and searchable format, and Commitizen gives contributors an easy, structured way to write proper commit messages without memorizing conventions. Together, they prevent chaotic commit logs, improve collaboration, power automated changelogs, and make long-term maintenance far less painful. + +- Husky hooks installed automatically through `prepare` script +- Commitlint enforces conventional commits +- Commitizen interactive prompts via: + +```bash +npm run commit +``` + +### 🌟 Jest Test Suite (Integration-Ready) + +- Jest fully configured for ESM +- Integration tests located in `tests/integration/api/v1/...` +- Tests call real Next.js API routes +- CI test flow automatically spins up services and runs migrations +- Scripts: + - `npm test` + - `npm run test:watch` + - `npm run test:ci` — runs Next.js + Jest concurrently + +### 🏅 GitHub Actions to ensure quality + +Made to ensure the project grows in a clean, secure and professional way, running this GitHub Action on all pull requests: + +- Automated tests +- Commit linting +- Code quality linting +- Code style linting + +If you or someone else ever make a bad PR, GitHub will let you know. + +![alt text](image.png) + +> Tip: You can make the tests Required for merge in GitHub's interface. That way, if some check don't pass, the Merge button won't be available, adding some extra security for the project. + +### 🐳 Dockerized Local Environment + +Docker Compose (`infra/compose.yaml`) that provides a PostgreSQL container. + +Service commands: + +- `npm run services:up` — start infrastructure containers +- `npm run services:stop` — stop containers +- `npm run services:down` — remove containers and volumes + +### 🐘 PostgreSQL + Migrations + +- PostgreSQL managed via Docker Compose +- Automatic database readiness check (`infra/scripts/wait-for-postgres.js`) +- Migration system powered by **node-pg-migrate** +- Migration files stored in `infra/migrations` +- Scripts: + - `npm run migrations:create` — create a new migration file + - `npm run migrations:up` — apply pending migrations + +### 🏘️ Next.js Application Structure + +- API routes organized under `pages/api/v1/...` +- Example endpoints (such as `status`) +- Ready-to-expand modular folder structure +- ESM-first setup (`"type": "module"` in package.json) + +### 🧼 ESLint + Prettier -Contributions are welcome! Please follow these steps: +- ESLint with Next.js, Jest, and Prettier integrations +- Prettier config prewired +- Linting commands: + - `npm run lint:eslint:check` + - `npm run lint:prettier:check` + - `npm run lint:prettier:fix` -1. Fork the repository. -2. Create a new branch for your feature or bug fix. -3. Make your changes and commit them using conventional commits. -4. Submit a pull request. +--- -## License +## 📜 License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +MIT License. diff --git a/image.png b/image.png new file mode 100644 index 0000000000000000000000000000000000000000..69fd274e6bebd8be2ed4f820ce2a91d803058559 GIT binary patch literal 49114 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

    vGJ2)hn(B2_#T0V67 zc+OU1VO_R-@E|TJElH?BLv}2^<8GL&o;U1m0^Det_^XV#h-%69w?F;p@z4SCV$DBT zOhv_Xey{QUhZuS`0awbYzyxlDY9EhlNkjROY}T+NIqBrQ z)=#D}R;!}^Ix%mkg(0n;JB6+K$k{%ghNg<^5~O9yh;#Q_gOK`+-K_olgE?ogA6Kk2 zfz?un!zre+3LnmN2H7rR&2_75Rz2QvcSL5sb1{CGy>C0Bk}Y6+bW}ToaQs0>zhI)B zzI&GI%h>SmQ_MN8u2gwfZDiPK7nBX|Fo}`FaaO)Y*tKKFmj`u%l)b_bfvcQf4v+%< ziq~0Bt9cp=5|t+)u34LDxyze<;^o=?itLx&aanK-aRgEglQj zdXnq*^|53xVrjd;;rC&}3+YtbN25egUF>lEku>8s)NAza5P+K-6_nCopa zF>wq)xO}=6(S&QVQ->7^;AtUzdq{n}-WG)l!hkUBQsuee(KCB$o?i?Ka+_eBRDE&W zQloeap- z^p?$LfyN)hU?e5V<00NAzuyo>6+fex<1cqEFh&&yiRLN(>0dCew#|=IrU)=8-_%HJ zd!l5~E!iiO_n3(qQz{8QJ9_mpu?1{D5nPvHQ;5rBA!Y#qRj!#~NqO>}l26|531=~b&w-*2ckZs+8zq}~c z$MY+;fFZ0-Y^BZ`D4z@9!UlS1M;YzQ?;Cjdv6+C>iTh;hg1M%3dCYB z)FU=2_g)@Fcmj1k#C1_>)4(#>W5&UtjwqtL%eWRUFkOB)hfKQ-vKpVi5kgw(M{zF) zx4@h;#=o>nV*h(k`P3Aon2fw6Jgs|qVNs5y@MRJ0^fx zDdkg+(~FnJP#51M&5T$EZvQ9%ur$=d`9Nd9OTq^1z&{$YOd~A@kXF4&kkfXdBW0!x zr&{58`e1*mdU-<^qVSR+=w`672oaSEsq*pZPm(`nQI<+1nu=60NzFIOA#SyGjnQhR zagGD>ez3 z&_K9Knt5cqXq=no?Waq}onJWhy+7N3xR^TRUD1spbq~AV52pMz@3tpu=}ercD5A|f zEkUv0D9Dqzj1{8mI5GzSBs*hi5A1<`vVlKsfFFb9x_9}bv46W8Z1iI~7 z4lo+j*wmA}`t9BoYq0nlqniOkNWz}H@R=vt(YGPb?vv4MaCXej;h=?y>YrZkaC+MH zBfoQ=7N5BBeL(W{ohydhVT&<8WE_`fqbq&cZm6=%%a!cZ!Dt-!`&By6gr8N??nYgf zw|KIx&Y!6V+q+YAk5jpTtzkItD*B9|ysRaQ1D)u{y{Aki*QD3W>0rW_(6^1C9SgaQ zmnqA(qI$6d`(KZk&7x+rI|g*xd|+t!i@s210^%#%M0M6Rk0Uqy)(V?Dw_B{MRf2c? zJ%LAaoz#PmllS{zZ*gy29bnP2)UbcYb__h*u3j0J#^X&V^ZgydU=9aFjL-K#gc51O zj@`5v@Q^bd?$BR2zqIPl{H44=x@qxdr9K|FUw?Nlsx$X`Fb2R&(kDi6o#cXVmy!1h zb|CKOa}O#4)ZPQU9Z4n6hDF?fzi^TYWSG;i0xXh*+gs#!BoH)Co~|VRjtH#!_LE=p z;?W6F6Dee_9&;VyIIW_BXeey?-z7y6QzqaV<@{t8Keo?q3 zw{s0X&FIfKw-%|f@Dn|>?*y4#q55XI4%eTeg2uM5af&)bN=+Uh;z`dI@Uj*-^vr$8 z#3U&GStIG%>q^3esX=D;K&N_0Q0Sga1T=}nOo9qJ6>=88N!9!UzAjnfq8OD1AewOI=eKv4{#r_A{Sa3kw3?#-YEBkU=mA2*X^$Dqgpw31Y{sD;1&#RL)eW>3EO2 zw+BRc&b;Rp+W1?2pPWD%h;^8nIwF2cxC&{~Nl{7R-YKn~Aoc-Gm2+Azc_dN4todLo zYSq-6I%F6fK;|qpdSx|iW=T|c`ahZYb8l==2!iSv|H!8{nm^UC-@@RV^Ye1E>}fn3 zzy^FVL(Vy_GxyeZx6}Xiho?KUaW8qosd{Iy%*x7N?<({i!w(s_{RcZHL~L0elBi>zR#ILiOYW4ypR4QH;W04wBoww$6Yp4+XO?%PuHSR7LFj}4@-NWJ%rIWh!Gv(0z` z9^~)bQ?f4g;-$0`9oevWt*T3sD&(g$$Ha=97 zI7!N(&v9By%~Z=5z{wze6=XKussM5Bn>o^!9VN)SjLw=3gIoJlx5#C(36mHMU`LN+$ktjZX|ccE{gav*zmf!)D- zXr_ra%7@f`9J9#Jw{Y4zFYj zI)} zW?p{LCg@Dj`5vucyA_XDSab;N<<7_{@e4c}g z7J7GLCEgGh>?;3b-_`n_*mX4$jafO3za)5^=FnZve|i2*&!fHOSeuja78TLJd1pHZ zeQz6tH*^n9Ce=xj48c&4*GgevSZi-qb%QfU%YFVBxuXEuEt#r}$P%;cxD!4(-u>a1kZS=ben(I9}c$8n7pR}21LJARt7N6P-W*y|l_oM`Z1PQd%n2-Mg?j{v* zB3NLoOk1uY90u&Btb2J3kmRH113dg4&ijrdagENY9O{f67tkXf8^AYLF=l;if2jO} zlOw0~MEvqIMQE_cxET1U`V(riV+mVBNfs&K8L(s@3+gx(jp~<9+@CEqLhQr9BX_9+ zy4T%jw)GWOi{Tr|hi|4T<4n-K<{v#X{*_!?+Tvb6;((#7mUJufnI_O#(9xV>t`)OO_W?_!Ml&YE(?nSE-_6lR2LPaxJSaxj)Z(_E$u+kA z{teOaJWIx(+L-}SGiHwNY&L-7Uv>Ezb82ig&IMx|IdL9n?QicEU$Utd%bvf(zju!} z!$OQY1;YP8!1XV+2n{d8F%#)OoyhY`jRngKynPh_ z#pTbK<6RE^m*w#xPsgZUyOrJZrmsqg=L!6W_Q+cT2Woe;@Q=9v$R2c=)N3z^PzB1KYTSWow~QL^^Hfn>?!QI5viJ@N~ zZ(z`ee>=@c*Z+I#m4*19{(>vLQUA{mE$R~aH`x3um7h%A$EtPSHs+R4tDmVrey{&k zDKae{xyAkNEm+>xG3RPVKJD?=kGxr2=8bGZJZ3!gL3(3npKHRrtSo5{zeTw4e08^b z_2HpxR{BWUOApTF{pL|&Z(4Z-+!nOw^pz%>$!gMTAqk^Qt6m zU=OTI>0s@sp*n0Dep#9&kd>JC-__6g?tgt_PfY1oM7`P3o^_*K8cZy^R6Znu|zn4Ogq;So6oDT+((Ae7xgZib~ z>EyT!`Nc}E(#tcDJkA5SQT|7OXS{TV`pX^g%k`kU90$@@QOMlywGcc^41$8W4W(fA z40)|72?573AEX9R<+p29p8-~??{#cC&R^}(f=Vj1&~(U!nDFBSWhR!6bL1||CKNW_ zVDw3sy26{Z9kho;@*K)pxMf%Gv?mNF9hlr}_oKPVX{&xz|_Qucp4vJ#jCdQ9kE) zFE-Jd2@tds)vi2`eVcf(JB(J_>1y`zo5m0YRkWiD6P0)uFrCjQ$cp`%+6)jzq-Ww% zr-$F4uT2}Gku)19;Sq+j_NXS%R8OBZikUdfB#uS_qROANqho_9?kwP7J*R+~{BQ;Q zkJ8pA(Y|~3U7y7DPc={BICfp5HoY%jX_i`FU8QeNV19 zl|oQO0&uLyo56+bEg)^5AnKpiBiD6`n9c4Gm`&a;WF@5|^ga0j-^M1x0qOxf&&^1U;y=)>td-c5k6P5j zbKgZ74zYOscOxETh~(Pk)_bAwfDrdvD_ObW{MJolR7rYzp|otRzVNjgt4j=spw6q3 zoi=u}an!!2#EY$*s)8g=`5HDmtAW}R$kt>Cpt#fXUGSc9 z1bV3*WgZV$wb;;&1S|b;Q1Q*$yY-fwehZn`=IZiORB_;T?F&4F>iniZ@oa>#I+H=x zeAl*cFwuwXjhoh^L|o$cqNzb-tqvZ}zsmi0uhE&bk?K{e;;cEDKkFa*tE4}1Ti9=` zK%?&!R;C`2IdRaW;-Ht0^S=1QX(Rt7y8bh)Rs8b$&JUS4L_c4W=1!m_F&cYo&wE$B z=H@-T2E~hee;Bz3=W6=4zh=$Y(w-PX->4*lsA8@;j5?1Ghcu>~O$wnf_K2!xf~Z21OD2)H*xZ z9zcwCw4mZIE~B7VN42iye6j&|*b}P3WMJ}W?eP(+P{-1?knXzs{8TAJ`0M`5EZNKy zAvN9;mDhKr^e(;`6EVZ3Gl4#B{5S~2g~DKK7Y{$zc=#}btg9Wo5o}O-qc5kooT3gZ z;urUKJ!3S9k_z2FHb8+ zp84rWF`lSbg@1Pcx(tI*JD5VKdv6%p5as6Qa3sApGE!Vs;_r@zBCq{jy2g&enwZOs zEvYnQ4zOfJ`Sp4htJv5KS&wi7{T*wBh~>21&Kn@;ET=*+Ng2~XE*Wy)%XSf@>xT@9 zd=LtyCRLg3oYal7h8NY-a`bF3!aq2ZLoi_3$s387*|e8673P*D?zB5b$tO9dqMZ#u ziK4OeZBUe3ZpwW?tx?^@oGs%^AD&3^@REmjg#iGS#KIPJftG-e@qQ{~&=DP((~^}< zt^C}_`F5f$nv?GxhQBa~Rjlj-J37UDYe~Aln6)N6KeyolICdr~?eI9?+w_ZJGy-R$ z34MP2efZA?`eErs@5i2g&N&rwS$ab54q)yYn$h>qv8hS5^mV|fX9a%4zT%+U)qQT_ zu)N**tbqanV5xvI$jh%D#tTcwCbunnO()rU3Z$tKccF`!BFV?u8p4}yh+Lz>U&;uxqL(T;p@fB#vIUtnwgQAi9>WC@)0m!5)ce0_#f-$C+X_q zWh8O390#QEGXiLt$7ihhHt$EN=!!l201H46h#z}6uLJAgaCBx4PWQ#u9s{5i|4Oo* z!Yll`WH`Dzf)yQr2QSd$(Ip)7hKGVR`%>IvNlaepVNkO~emr zi4SOy{AGpp_0M53heGBLM1)($1yEwQkWo{1T9!tLvTS%j7m2>eu=}lwFKnhk+iUcW ze*Xa=o$(H{n_d;dXWR3Wz<`aRs)dOxwlJzmUI*lAb*ph7BY7v46zsJONoRq{Z%@W) ztnI%(a?P_1!h;n<`V_z+A?w=6S4uTjIjPq^l=}-yA#7k+h>mL3*K^f`_h`2`VDV3B z%>IW2XGq7(tJ@t2z~p4oJ^g?vgkb-imIMMv#Uiw6<&P%B2a%qemNZg*4czRLA4&7t zR3(D(Som9q|1e=MrHc!?#{XjS@b$oer%v*ylo@nKd{X=-ZAEoNq9!KPsZ8@f$AFOj zNW9oIcy@lmL|kbio|hi1e(@G>@VaQLc`8u|^_5Fz+?Vyi2Z`Y4OF;U5@_;0=qQk7m z0#?kWGlZMc88QcO);$QlDxZBd)hqc#5TN6wb5Y1Tkx_d!p7?Ko4#BCrl$du_>s5MXVB?z@2GC?XHoSF zZR_6PK>LJ&R>BfVqHdJV<)8EYvTG09iP7bpPRHIWjkleFf{X}uRP#YNeR@f?Y@JVU zJH-pz5J=JcHfAA4kEvF)F1zja)u1mtAS}_dUZFex8dKH|M8mrc3R*e zwmku0Ht1(6nx$38@qLZhJx->Vu?hLcUxlS=_lxPj_|8q9>|1Y{7|1Z_^cc>%^NCE<$ms|k+laWvm KuM#!<_P+oL`7#jz literal 0 HcmV?d00001 diff --git a/pages/index.js b/pages/index.js index 3cd3ddb..61248bb 100644 --- a/pages/index.js +++ b/pages/index.js @@ -10,142 +10,18 @@ function Home() { >

    SaaS Base

    -

    +

    It is much easier to build your SaaS from here, as this is made to cover the foundational stuff well, and to be extensible 😉 -

    + -

    It comes with:

    - -
      -
    • - A .editorconfig file which configures some rules for your - editor code. -
    • - -
    • - Prettier and .prettierignore configured. -
    • - -
    • - ESLint configured, including its functionality together with Prettier, - Jest and Next.js. -
    • - -
    • - Jest configured, including a jest.config.js file that - configures jest to be able to use absolute imports, to load the{" "} - next.config.js file and to use the{" "} - .env.development variables in the test environment. -
    • - -
    • - Folder structure that considers future API versioning:{" "} - /pages/api/v1/... -
    • - -
    • - /api/v1/status endpoint that, upon a GET request, returns - the current system status, including: database (postgres) version, - database maximum accepted connections, and database opened - connections. -
    • - -
    • - /api/v1/migrations endpoint that upon a GET request - returns all pending migrations that are not applied; and upon a POST - request runs all migrations. -
    • - -
    • - A Docker Compose file at /infra/compose.yaml that starts - a Postgres instance, using credentials in{" "} - .env.development. -
    • - -
    • - A /infra/database.js module with a query() - method that receives a query, establishes the connection with the - database and returns the query result, closing the connection - afterwards and catching errors. Also the - getNewClient() - {" "} - method that returns a connected database client - which is used in{" "} - /pages/api/v1/migrations to get and run database - migrations. -
    • - -
    • - A jsconfig.json file that defines the{" "} - baseUrl of the project, so you can use absolute imports. -
    • - -
    • - npm run dev command that starts the postgres instance - with docker compose, await for the database to be ready, then run the - migrations, and only then starts the development server - yes, all - with one command :) -
    • - -
    • - npm run test command that starts the docker postgres - instance, wait for the database to be ready for connections, then run - the development server concurrently with the tests. -
    • - -
    • - And other commands like{" "} - - posttest, test:watch, services:up, services:stop, services:down, - services:wait:database, migrations:create, migrations:up, - lint:prettier:check, lint:prettier:fix, lint:elint:check, prepare - {" "} - and commit. -
    • - -
    • - /infra/scripts/orchestrator.js with a{" "} - waitForAllServices method that checks the readiness of - the api, and a clearDatabase() method that... yes, clears - the database - it is used for the tests. -
    • - -
    • - GitHub Actions, including: code style linting with Prettier, code - quality linting with ESLint, commit linting with{" "} - commitlint and automated tests with Jest. All these jobs - will run once a pull request is opened, and will warn you of any - problems. -
    • - -
    • - Pre-commit routine configured with Husky that will check if your - commit message is compatible with the Conventional Commit - specification. -
    • - -
    • - commitlint that will check your commits and{" "} - commitizen so you can ensure Conventional Commit messages - through the npm run commit command. -
    • - -
    • And other stuff that I may have forgotten to include here.
    • -
    - - - Please, help your fellow developer to ascend in its carreer and{" "} +

    + Please, help your fellow developer and{" "} give the project a star on GitHub {" "} 🍻 - - -
    -
    -
    - - Thank you for using SaaS Base. +

    ); }