feat: configurable initial user

This commit is contained in:
Dhruwang
2025-05-06 15:07:41 +05:30
parent a1df10eb09
commit 23706a935d
15 changed files with 309 additions and 20 deletions

View File

@@ -164,4 +164,6 @@ CMD if [ "${DOCKER_CRON_ENABLED:-1}" = "1" ]; then \
fi; \
(cd packages/database && npm run db:migrate:deploy) && \
(cd packages/database && npm run db:create-saml-database:deploy) && \
(cd packages/database && npm run db:initial-user-setup:deploy) && \
exec node apps/web/server.js

View File

@@ -286,3 +286,9 @@ export const SENTRY_DSN = env.SENTRY_DSN;
export const PROMETHEUS_ENABLED = env.PROMETHEUS_ENABLED === "1";
export const DISABLE_USER_MANAGEMENT = env.DISABLE_USER_MANAGEMENT === "1";
//initial setup variables
export const INITIAL_USER_EMAIL = env.INITIAL_USER_EMAIL;
export const INITIAL_USER_PASSWORD = env.INITIAL_USER_PASSWORD;
export const INITIAL_ORGANIZATION_NAME = env.INITIAL_ORGANIZATION_NAME;
export const INITIAL_PROJECT_NAME = env.INITIAL_PROJECT_NAME;

View File

@@ -1,5 +1,6 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
import { ZUserEmail, ZUserPassword } from "@formbricks/types/user";
export const env = createEnv({
/*
@@ -113,6 +114,10 @@ export const env = createEnv({
PROMETHEUS_EXPORTER_PORT: z.string().optional(),
PROMETHEUS_ENABLED: z.enum(["1", "0"]).optional(),
DISABLE_USER_MANAGEMENT: z.enum(["1", "0"]).optional(),
INITIAL_USER_EMAIL: ZUserEmail.optional(),
INITIAL_USER_PASSWORD: ZUserPassword.optional(),
INITIAL_ORGANIZATION_NAME: z.string().optional(),
INITIAL_PROJECT_NAME: z.string().optional(),
},
/*
@@ -212,5 +217,9 @@ export const env = createEnv({
PROMETHEUS_ENABLED: process.env.PROMETHEUS_ENABLED,
PROMETHEUS_EXPORTER_PORT: process.env.PROMETHEUS_EXPORTER_PORT,
DISABLE_USER_MANAGEMENT: process.env.DISABLE_USER_MANAGEMENT,
INITIAL_USER_EMAIL: process.env.INITIAL_USER_EMAIL,
INITIAL_USER_PASSWORD: process.env.INITIAL_USER_PASSWORD,
INITIAL_ORGANIZATION_NAME: process.env.INITIAL_ORGANIZATION_NAME,
INITIAL_PROJECT_NAME: process.env.INITIAL_PROJECT_NAME,
},
});

View File

@@ -1,6 +1,6 @@
{
"auth": {
"continue_with_azure": "Login mit Azure",
"continue_with_azure": "Weiter mit Microsoft",
"continue_with_email": "Login mit E-Mail",
"continue_with_github": "Login mit GitHub",
"continue_with_google": "Login mit Google",

View File

@@ -1,6 +1,6 @@
{
"auth": {
"continue_with_azure": "Continue with Azure",
"continue_with_azure": "Continue with Microsoft",
"continue_with_email": "Continue with Email",
"continue_with_github": "Continue with GitHub",
"continue_with_google": "Continue with Google",

View File

@@ -1,6 +1,6 @@
{
"auth": {
"continue_with_azure": "Continuer avec Azure",
"continue_with_azure": "Continuer avec Microsoft",
"continue_with_email": "Continuer avec l'e-mail",
"continue_with_github": "Continuer avec GitHub",
"continue_with_google": "Continuer avec Google",

View File

@@ -1,6 +1,6 @@
{
"auth": {
"continue_with_azure": "Continuar com Azure",
"continue_with_azure": "Continuar com Microsoft",
"continue_with_email": "Continuar com o Email",
"continue_with_github": "Continuar com o GitHub",
"continue_with_google": "Continuar com o Google",

View File

@@ -1,6 +1,6 @@
{
"auth": {
"continue_with_azure": "Continuar com Azure",
"continue_with_azure": "Continuar com Microsoft",
"continue_with_email": "Continuar com Email",
"continue_with_github": "Continuar com GitHub",
"continue_with_google": "Continuar com Google",

View File

@@ -1,6 +1,6 @@
{
"auth": {
"continue_with_azure": "使用 Azure 繼續",
"continue_with_azure": "繼續使用 Microsoft",
"continue_with_email": "使用電子郵件繼續",
"continue_with_github": "使用 GitHub 繼續",
"continue_with_google": "使用 Google 繼續",

View File

@@ -0,0 +1,85 @@
---
title: "Automated Setup"
description: "Automatically create the first user and organization with environment variables"
icon: "robot"
---
# Automated Setup
When deploying Formbricks in production environments, it's often useful to automate the initial setup process. This guide explains how to use environment variables to automatically create the first user, organization, and project during startup.
## Overview
Formbricks supports automatic creation of the initial administrator user on first startup through environment variables. This is particularly useful for:
- Automating deployments of multiple instances
- Setting up Formbricks in Docker or Kubernetes environments
- Integrating with infrastructure as code tools
- Streamlining CI/CD pipelines
## Configuration
To enable automated setup, set the following environment variables before starting Formbricks:
### Required Variables
| Variable | Description | Example |
| --- | --- | --- |
| `INITIAL_USER_EMAIL` | The email address for the admin user | `admin@example.com` |
| `INITIAL_USER_PASSWORD` | The password for the admin user | `your-secure-password` |
### Optional Variables
| Variable | Description | Default Value | Example |
| --- | --- | --- | --- |
| `INITIAL_ORGANIZATION_NAME` | The name of the initial organization | `My Organization` | `Acme Corp` |
| `INITIAL_PROJECT_NAME` | The name of the initial project | *none* | `Customer Feedback` |
## How It Works
When Formbricks starts up, it checks if these environment variables are defined and if the database is empty (no users exist). If both conditions are met, it will:
1. Create an administrator user with the provided email and password
2. Create an organization with the provided or default name
3. Create a project if `INITIAL_PROJECT_NAME` is provided
4. Set up both development and production environments for the project
After successful setup, you'll be able to log in with the provided credentials immediately.
## Example Docker Compose Configuration
Here's an example of how to configure these variables in a Docker Compose file:
```yaml
version: "3.3"
services:
formbricks:
image: ghcr.io/formbricks/formbricks:latest
environment:
# Required environment variables
WEBAPP_URL: "https://formbricks.example.com"
NEXTAUTH_URL: "https://formbricks.example.com"
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public"
NEXTAUTH_SECRET: "your-nextauth-secret"
ENCRYPTION_KEY: "your-encryption-key"
CRON_SECRET: "your-cron-secret"
# Automated setup variables
INITIAL_USER_EMAIL: "admin@example.com"
INITIAL_USER_PASSWORD: "your-secure-password"
INITIAL_ORGANIZATION_NAME: "Acme Corp"
INITIAL_PROJECT_NAME: "Customer Feedback"
```
## Security Considerations
- Use a strong, unique password for the admin user
- Consider removing or changing the environment variables after the initial setup
- In production environments, ensure these variables are securely managed and not exposed in logs or configuration files
- Use Docker secrets or environment variable management systems in container orchestration platforms
## Limitations
- The automated setup only works if no users exist in the database
- If the setup process is interrupted, you may need to manually clean up the database before retrying
- The feature cannot be used to create additional users after the first user exists

View File

@@ -67,8 +67,13 @@ These variables are present inside your machines docker-compose file. Restart
| PROMETHEUS_ENABLED | Enables Prometheus metrics if set to 1. | optional | |
| PROMETHEUS_EXPORTER_PORT | Port for Prometheus metrics. | optional | 9090 |
| DOCKER_CRON_ENABLED | Controls whether cron jobs run in the Docker image. Set to 0 to disable (useful for cluster setups). | optional | 1 |
| SURVEY_URL | Set this to change the domain of the survey. | optional | WEBAPP_URL
| SENTRY_DSN | Set this to track errors and monitor performance in Sentry. | optional |
| SENTRY_AUTH_TOKEN | Set this if you want to make errors more readable in Sentry. | optional |
| DISABLE_USER_MANAGEMENT | Set this to hide the user management UI. | optional |
| SURVEY_URL | Set this to change the domain of the survey. | optional | WEBAPP_URL |
| SENTRY_DSN | Set this to track errors and monitor performance in Sentry. | optional | |
| SENTRY_AUTH_TOKEN | Set this if you want to make errors more readable in Sentry. | optional | |
| DISABLE_USER_MANAGEMENT | Set this to hide the user management UI. | optional | |
| INITIAL_USER_EMAIL | Set this to create an initial user with the given email. | optional | |
| INITIAL_USER_PASSWORD | Set this to create an initial user with the given password. | optional | |
| INITIAL_USER_ORGANIZATION | Set this to create an initial user with the given organization. | optional | |
| INITIAL_USER_PROJECT | Set this to create an initial user with the given project. | optional | |
Note: If you want to configure something that is not possible via above, please open an issue on our GitHub repo here or reach out to us on Github Discussions and well try our best to work out a solution with you.

View File

@@ -12,8 +12,10 @@
"db:migrate:dev": "dotenv -e ../../.env -- sh -c \"pnpm prisma generate && tsx ./src/scripts/apply-migrations.ts\"",
"db:create-saml-database:deploy": "env SAML_DATABASE_URL=\"${SAML_DATABASE_URL}\" tsx ./src/scripts/create-saml-database.ts",
"db:create-saml-database:dev": "dotenv -e ../../.env -- tsx ./src/scripts/create-saml-database.ts",
"db:initial-user-setup:deploy": "env SAML_DATABASE_URL=\"${SAML_DATABASE_URL}\" tsx ./src/scripts/initial-user-setup.ts",
"db:initial-user-setup:dev": "dotenv -e ../../.env -- tsx ./src/scripts/initial-user-setup.ts",
"db:push": "prisma db push --accept-data-loss",
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev",
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev && pnpm db:initial-user-setup:dev",
"db:start": "pnpm db:setup",
"format": "prisma format",
"generate": "prisma generate",
@@ -26,6 +28,7 @@
"@formbricks/logger": "workspace:*",
"@prisma/client": "6.6.0",
"@prisma/extension-accelerate": "1.3.0",
"bcryptjs": "3.0.2",
"dotenv-cli": "8.0.0",
"zod-openapi": "4.2.4"
},

View File

@@ -0,0 +1,172 @@
import { hash } from "bcryptjs";
import { logger } from "@formbricks/logger";
import { ZProject } from "../../../types/project";
import { ZUserEmail, ZUserPassword } from "../../../types/user";
import { ZOrganization } from "../../zod/organizations";
import { prisma } from "../client";
const { INITIAL_USER_EMAIL, INITIAL_USER_PASSWORD, INITIAL_ORGANIZATION_NAME, INITIAL_PROJECT_NAME } =
process.env;
export const isFreshInstance = async (): Promise<boolean> => {
try {
const userCount = await prisma.user.count();
const organizationCount = await prisma.organization.count();
return userCount === 0 && organizationCount === 0;
} catch (error) {
logger.error("Error checking if instance is fresh:", error);
return false;
}
};
const isValidEmail = (email: string): boolean => {
const parseResult = ZUserEmail.safeParse(email);
return parseResult.success;
};
const isValidPassword = (password: string): boolean => {
const parseResult = ZUserPassword.safeParse(password);
return parseResult.success;
};
const isValidOrganizationName = (name: string): boolean => {
const parseResult = ZOrganization.pick({ name: true }).safeParse({ name });
return parseResult.success;
};
const isValidProjectName = (name: string): boolean => {
const parseResult = ZProject.pick({ name: true }).safeParse({ name });
return parseResult.success;
};
const validateEnvironmentVariables = (): boolean => {
if (INITIAL_USER_EMAIL && !isValidEmail(INITIAL_USER_EMAIL)) {
logger.error("Invalid email format. Please provide a valid email.");
return false;
}
if (INITIAL_USER_PASSWORD && !isValidPassword(INITIAL_USER_PASSWORD)) {
logger.error("Invalid password format. Please provide a valid password.");
return false;
}
if (INITIAL_ORGANIZATION_NAME && !isValidOrganizationName(INITIAL_ORGANIZATION_NAME)) {
logger.error("Invalid organization name format. Please provide a valid organization name.");
return false;
}
if (INITIAL_PROJECT_NAME && !isValidProjectName(INITIAL_PROJECT_NAME)) {
logger.error("Invalid project name format. Please provide a valid project name.");
return false;
}
return true;
};
const createInitialUser = async (
email: string,
password: string,
organizationName?: string,
projectName?: string
): Promise<void> => {
const hashedPassword = await hash(password, 12);
if (!organizationName) {
// Create only a user without an organization
await prisma.user.create({
data: {
name: "Admin",
email: email.toLowerCase(),
password: hashedPassword,
emailVerified: new Date(),
locale: "en-US",
},
});
return;
}
// Create user with organization
await prisma.user.create({
data: {
name: "Admin",
email: email.toLowerCase(),
password: hashedPassword,
emailVerified: new Date(),
locale: "en-US",
memberships: {
create: {
role: "owner",
accepted: true,
organization: {
create: {
name: organizationName,
billing: {
plan: "free",
limits: { projects: 3, monthly: { responses: 1500, miu: 2000 } },
stripeCustomerId: null,
periodStart: new Date(),
period: "monthly",
},
...(projectName && {
projects: {
create: {
name: projectName,
environments: {
create: [{ type: "development" }, { type: "production" }],
},
},
},
}),
},
},
},
},
},
});
};
const initEnvironment = async (): Promise<boolean> => {
try {
logger.info("Checking if initial environment setup is needed...");
if (!validateEnvironmentVariables()) {
return false;
}
if (!INITIAL_USER_EMAIL || !INITIAL_USER_PASSWORD) {
logger.info("No initial user credentials provided. Skipping automated setup.");
return true;
}
const freshInstance = await isFreshInstance();
if (!freshInstance) {
logger.info("Not a fresh instance (users or organizations exist). Skipping initial setup.");
return true;
}
logger.info("Fresh instance detected. Creating initial admin user...");
await createInitialUser(
INITIAL_USER_EMAIL,
INITIAL_USER_PASSWORD,
INITIAL_ORGANIZATION_NAME,
INITIAL_PROJECT_NAME
);
logger.info(`
✅ Successfully created initial admin user${INITIAL_ORGANIZATION_NAME ? " and organization" : ""}:
- Email: ${INITIAL_USER_EMAIL}
${INITIAL_ORGANIZATION_NAME ? `- Organization: ${INITIAL_ORGANIZATION_NAME}` : ""}
${INITIAL_PROJECT_NAME && INITIAL_ORGANIZATION_NAME ? `- Project: ${INITIAL_PROJECT_NAME}` : ""}
You can now log in with the credentials provided in the environment variables.
`);
return true;
} catch (error) {
logger.error("Error during initial environment setup:", error);
return false;
}
};
initEnvironment()
.then(() => {
process.exit(0);
})
.catch((error: unknown) => {
logger.error(error, "Error creating SAML database");
});

19
pnpm-lock.yaml generated
View File

@@ -669,6 +669,9 @@ importers:
'@prisma/extension-accelerate':
specifier: 1.3.0
version: 1.3.0(@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3))
bcryptjs:
specifier: 3.0.2
version: 3.0.2
dotenv-cli:
specifier: 8.0.0
version: 8.0.0
@@ -15314,7 +15317,7 @@ snapshots:
'@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0)(typescript@5.8.3)
'@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.8.3)
eslint-config-prettier: 9.1.0(eslint@8.57.0)
eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.31.0)
eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.0)
eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0)
@@ -16805,7 +16808,7 @@ snapshots:
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.0)
eslint-plugin-react: 7.37.5(eslint@8.57.0)
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.0)
@@ -16830,9 +16833,9 @@ snapshots:
eslint-plugin-turbo: 2.5.0(eslint@8.57.0)(turbo@2.5.0)
turbo: 2.5.0
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0):
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0)):
dependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0)
eslint-import-resolver-node@0.3.9:
dependencies:
@@ -16853,11 +16856,11 @@ snapshots:
tinyglobby: 0.2.13
unrs-resolver: 1.6.2
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0)
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0):
eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -16909,7 +16912,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -16927,7 +16930,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@8.57.0)(typescript@5.8.3))(eslint@8.57.0):
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@8.57.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8

View File

@@ -199,7 +199,11 @@
"UNKEY_ROOT_KEY",
"PROMETHEUS_ENABLED",
"PROMETHEUS_EXPORTER_PORT",
"DISABLE_USER_MANAGEMENT"
"DISABLE_USER_MANAGEMENT",
"INITIAL_USER_EMAIL",
"INITIAL_USER_PASSWORD",
"INITIAL_ORGANIZATION_NAME",
"INITIAL_PROJECT_NAME"
],
"outputs": ["dist/**", ".next/**"]
},