From 0f2e2760931c991a9bb456a822ca261057459336 Mon Sep 17 00:00:00 2001 From: kinhdev-sandbox Date: Thu, 29 Jan 2026 09:05:06 +0700 Subject: [PATCH 1/3] chore: Updated docs --- public/screenshots/google-oauth.png | Bin 0 -> 60616 bytes src/app/_components/NumberOfContent.jsx | 4 +- .../self-hosting/docker/advanced-setup.mdx | 216 ++++++++++++------ 3 files changed, 143 insertions(+), 77 deletions(-) create mode 100644 public/screenshots/google-oauth.png diff --git a/public/screenshots/google-oauth.png b/public/screenshots/google-oauth.png new file mode 100644 index 0000000000000000000000000000000000000000..17d0f89cc5d26a2f43811e3302057a02451d2f50 GIT binary patch literal 60616 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 literal 0 HcmV?d00001 diff --git a/src/app/_components/NumberOfContent.jsx b/src/app/_components/NumberOfContent.jsx index 98da1bd..69a03c0 100644 --- a/src/app/_components/NumberOfContent.jsx +++ b/src/app/_components/NumberOfContent.jsx @@ -10,7 +10,7 @@ export const NumberOfContent = ({ }) => { return (
-
+
{number}
@@ -37,7 +37,7 @@ export const NumberOfContent = ({ export const NumberWithMdx = ({ number, children, className }) => { return (
-
+
{number}
diff --git a/src/content/getting-started/self-hosting/docker/advanced-setup.mdx b/src/content/getting-started/self-hosting/docker/advanced-setup.mdx index 1c5f903..18e4313 100644 --- a/src/content/getting-started/self-hosting/docker/advanced-setup.mdx +++ b/src/content/getting-started/self-hosting/docker/advanced-setup.mdx @@ -2,6 +2,7 @@ import { Steps } from "nextra/components" import { Table } from 'nextra/components' import { Callout } from 'nextra/components' import { CardInfo } from "@/app/_components/CardInfo.jsx" +import { Tabs } from 'nextra/components' import { NumberOfContent, @@ -118,10 +119,35 @@ cp .env.example .env docker compose pull ``` -## Configuring and securing SpaceDF +## Configuration & Security -The `.env.example` file includes sample passwords and keys for reference only. -You must replace these values before starting SpaceDF in a self-hosted environment. +Before starting SpaceDF, you need to set up the environment variables. + + + + + Copy the `.env.example` file in the root directory. + + + Rename the copy to `.env` + + + Update the values in `.env` following the guide below. + + + + + Run the following command in your project root directory: + + ```bash copy + # Switch to your project directory + cd spacedf-core + + # Copy the env vars + cp .env.example .env + ``` + + Review the configuration options below and make sure all secret values are set before starting SpaceDF. @@ -162,8 +188,8 @@ RABBITMQ_DEFAULT_USER=your_username RABBITMQ_DEFAULT_PASS=your_password ``` -- `RABBITMQ_DEFAULT_USER` - The username SpaceDF uses to connect to RabbitMQ. -- `RABBITMQ_DEFAULT_PASS` - The password for the RabbitMQ user above. +`RABBITMQ_DEFAULT_USER` - The username SpaceDF uses to connect to RabbitMQ.
+`RABBITMQ_DEFAULT_PASS` - The password for the RabbitMQ user above. Do not use simple or common passwords. This account controls access to your message queue.
@@ -184,15 +210,41 @@ openssl genrsa -out private_key.pem 2048 openssl rsa -in private_key.pem -pubout -out public_key.pem ``` - Copy the contents of each file into your `.env` file: + Copy the key contents from your local machine
+ Choose the command based on your operating system: + + + ```bash copy + pbcopy < private_key.pem + pbcopy < public_key.pem + ``` + + + + ```powershell copy + Get-Content private_key.pem | Set-Clipboard + Get-Content public_key.pem | Set-Clipboard + ``` + + + + ```bash copy + cat private_key.pem | xclip -selection clipboard + cat public_key.pem | xclip -selection clipboard + ``` + + +
+ + Paste the keys into your `.env` file: ```bash copy JWT_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----... JWT_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----... ``` -- `JWT_PRIVATE_KEY` - Signs authentication tokens (***Keep this key secret.***). -- `JWT_PUBLIC_KEY` - Verifies authentication tokens. This key can be shared with other services if needed. +`JWT_PRIVATE_KEY` - Signs authentication tokens (***Keep this key secret.***).
+`JWT_PUBLIC_KEY` - Verifies authentication tokens. This key can be shared with other services if needed. > Make sure the keys are pasted correctly and not broken across lines. --- ##### Google OAuth @@ -216,6 +268,8 @@ To enable Google login, you need to create OAuth credentials in the ***Google Cl Create an **OAuth 2.0 Client ID:**
Application type: **Web application**
+ + *If you see a warning about **OAuth consent screen** or **branding** not being configured, you must complete the **OAuth consent screen setup** first before creating the Client ID.*
Authorized redirect URI: (`GOOGLE_CALLBACK_URL`)
@@ -234,13 +288,14 @@ To enable Google login, you need to create OAuth credentials in the ***Google Cl ```bash copy # Replace with your own values. -GOOGLE_CALLBACK_URL=https://spacedf.example.com/auth/google/callback -GOOGLE_CLIENT_ID=1234567890-abcxyz.apps.googleusercontent.com #(Step 5) +GOOGLE_CALLBACK_URL=your_google_callback_uri +GOOGLE_CLIENT_ID=your_google_client_id #(Step 5) GOOGLE_CLIENT_SECRET=your_google_client_secret #(Step 5) ``` -- `GOOGLE_CALLBACK_URL` - The URL Google redirects users back to after successful login. -- `GOOGLE_CLIENT_ID` - Identifies your application to Google. -- `GOOGLE_CLIENT_SECRET` - A private key used by SpaceDF to securely communicate with Google (***Keep this value secret.***) +`GOOGLE_CALLBACK_URL` - The URL Google redirects users back to after successful login.
+`GOOGLE_CLIENT_ID` - Identifies your application to Google.
+`GOOGLE_CLIENT_SECRET` - A private key used by SpaceDF to securely communicate with Google (***Keep this value secret.***) + Use HTTPS for the callback URL in production. @@ -256,44 +311,55 @@ Apple sign-in support is planned but not yet supported in SpaceDF. ```bash copy # Apple OAuth (reserved for future use) -APPLE_CLIENT_ID=__APPLE_CLIENT_ID__ -APPLE_CLIENT_SECRET=__APPLE_CLIENT_SECRET__ -APPLE_CLIENT_KEY=__APPLE_CLIENT_KEY__ -APPLE_CERTIFICATE_KEY=__APPLE_CERTIFICATE_KEY__ +APPLE_CLIENT_ID=your_apple_client_id +APPLE_CLIENT_SECRET=your_apple_client_secrect +APPLE_CLIENT_KEY=your_apple_client_key +APPLE_CERTIFICATE_KEY=your_apple_certificate_key ``` --- ##### Auth Service > The Auth Service is responsible for user authentication, authorization, and tenant management in SpaceDF. -Set the following values in your `.env` file. +**`AUTH_POSTGRES_PASSWORD`** + +The password used by the Auth Service to connect to its *PostgreSQL database*. (*Use a strong and unique password.*) + ```bash copy -# Replace with your own values. -AUTH_POSTGRES_PASSWORD=__AUTH_POSTGRES_PASSWORD__ -AUTH_SECRET_KEY=__AUTH_SECRET_KEY__ -DEFAULT_TENANT_HOST=__DEFAULT_TENANT_HOST__ -ROOT_API_KEY=__ROOT_API_KEY__ +AUTH_POSTGRES_PASSWORD=your_auth_postgres_password ``` -- `AUTH_POSTGRES_PASSWORD` - The password used by the Auth Service to connect to its ***PostgreSQL database***. (***Use a strong and unique password.***) -- `AUTH_SECRET_KEY` - A secret key used to sign and validate authentication-related data. (***Keep this value private.***) -- `DEFAULT_TENANT_HOST` - The default domain or host assigned to the initial tenant. This is usually your main application domain. -- `ROOT_API_KEY` - A master API key with full access to the Auth Service. Used for administrative or internal operations only. -**Secret keys**: Generate secure random values for secret keys: +**`AUTH_SECRET_KEY`** + +A secret key used to sign and validate authentication-related data. (*Keep this value private.*) + +Generate secure random values for secret keys: ```bash copy openssl rand -hex 32 ``` -Use the generated value for: + +Paste the generated Secret Key into your `.env` file: ```bash copy -AUTH_SECRET_KEY=generated_secret_value -ROOT_API_KEY=generated_root_api_key +AUTH_SECRET_KEY=your_auth_secret_key ``` -**Default tenant host** +**`DEFAULT_TENANT_HOST`** + +The default domain or host assigned to the initial tenant. This is usually your main application domain. + + + + ```bash copy + DEFAULT_TENANT_HOST=localhost + ``` + + + + ```bash copy + DEFAULT_TENANT_HOST=your_api_domain + ``` + + -Set this to the domain or host where SpaceDF will be accessed: -```bash copy -DEFAULT_TENANT_HOST=app.spacedf.example -``` --- ##### S3 Service > The Amazon S3 service is used by SpaceDF to store files such as uploads, assets, and generated data. @@ -354,16 +420,16 @@ Use IAM policies with **minimum required permissions**. **Set values in `.env`** ```bash copy - # Replace with your own values. - AWS_ACCESS_KEY_ID=AKIAXXXXXXXX - AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXX +# Replace with your own values. +AWS_ACCESS_KEY_ID=your_access_key_id #IAM credentials (Step 4) +AWS_SECRET_ACCESS_KEY=your_secret_access_key #IAM credentials (Step 4) - AWS_STORAGE_BUCKET_NAME=spacedf-storage - AWS_REGION=ap-southeast-1 +AWS_STORAGE_BUCKET_NAME=your_aws_storage_bucket_name #S3 bucket (Step 3) +AWS_REGION=your_aws_region #S3 bucket (Step 3) ``` - `AWS_ACCESS_KEY_ID` - The access key used by SpaceDF to authenticate with S3. -- `AWS_SECRET_ACCESS_KEY` - The secret key paired with the access key above. (***Keep this value private***) +- `AWS_SECRET_ACCESS_KEY` - The secret key paired with the access key above. (*Keep this value private*) - `AWS_STORAGE_BUCKET_NAME` - The name of the S3 bucket where SpaceDF stores files. - `AWS_REGION` - The AWS region where the S3 bucket is located. (e.g., `us-east-1`, `ap-southeast-1`). --- @@ -404,55 +470,56 @@ REDIS_HOST="redis://redis:6379/1" ##### Dashboard Service > The Dashboard Service provides the web interface for managing SpaceDF, including administration and monitoring features. -**Database password** +**`DASHBOARD_POSTGRES_PASSWORD`** + +The password used by the Dashboard Service to connect to its PostgreSQL database. (*Use a strong and unique password*.) -Choose a strong password for the Dashboard Service database: ```bash copy -# Replace with your own values. -DASHBOARD_POSTGRES_PASSWORD=change_this_to_a_secure_password +DASHBOARD_POSTGRES_PASSWORD=your_postgres_password ``` -`DASHBOARD_POSTGRES_PASSWORD` - The password used by the Dashboard Service to connect to its PostgreSQL database. (***Use a strong and unique password***.) -**Secret key**: Generate a secure random value: +**`DASHBOARD_SECRET_KEY`** + +A secret key used to sign and validate authentication-related data. (*Keep this value private.*) + +Generate secure random values for secret keys: ```bash openssl rand -hex 32 ``` -Set it in your `.env` file: +Paste the generated Secret Key into your `.env` file: ```bash copy -# Replace with your own values. -DASHBOARD_SECRET_KEY=generated_secret_value +DASHBOARD_SECRET_KEY=your_secret_key ``` -`DASHBOARD_SECRET_KEY` - A secret key used to sign and protect dashboard-related sessions and data. (***Keep this value private.***) --- ##### Device Service > The Device Service manages devices, device data, and communication with telemetry-related services in SpaceDF. -**Database password** +**`DEVICE_POSTGRES_PASSWORD`** + +The password used by the Device Service to connect to its PostgreSQL database. (*Use a strong and unique password*.) -Choose a strong password for the Device Service database: ```bash copy -# Replace with your own values. -DEVICE_POSTGRES_PASSWORD=change_this_to_a_secure_password +DEVICE_POSTGRES_PASSWORD=your_postgres_password ``` -`DEVICE_POSTGRES_PASSWORD` - The password used by the Device Service to connect to its PostgreSQL database. (***Use a strong and unique password***.) +**`DEVICE_SECRET_KEY`** -**Secret key**: Generate a secure random value: +A secret key used to sign and validate authentication-related data. (*Keep this value private.*) + +Generate secure random values for secret keys: ```bash openssl rand -hex 32 ``` -Set it in your `.env` file: - +Paste the generated Secret Key into your `.env` file: ```bash copy -# Replace with your own values. -DEVICE_SECRET_KEY=generated_secret_value +DEVICE_SECRET_KEY=your_secret_key ``` -`DEVICE_SECRET_KEY` - A secret key used to sign and protect device-related data and requests. (***Keep this value private.***) +--- -**Telemetry service URL** +##### Telemetry service Defines the internal Docker service address that SpaceDF uses to communicate with the Telemetry Service. @@ -460,7 +527,7 @@ When using the **default Docker setup**, the Telemetry Service runs as a Docker ```bash copy # Default (recommended) -TELEMETRY_SERVICE_URL=http://telemetry-service:8080 +TELEMETRY_SERVICE_URL=http://telemetry:8080 ``` +`EMQX_USERNAME` - The username SpaceDF uses to authenticate with the EMQX broker.
+`EMQX_PASSWORD` - The password for the EMQX user above. (*Use a strong and unique password*.) --- ##### Broker Bridge Service @@ -496,18 +563,17 @@ EMQX_PASSWORD=change_this_to_a_secure_password **Broker credentials** ```bash copy # Replace with your own values. -MQTT_BROKER_BRIDGE_USERNAME=bridge-user -MQTT_BROKER_BRIDGE_PASSWORD=change_this_to_a_secure_password +MQTT_BROKER_BRIDGE_USERNAME=your_username +MQTT_BROKER_BRIDGE_PASSWORD=your_password ``` -- `MQTT_BROKER_BRIDGE_USERNAME` - The username used to authenticate with the external MQTT broker. -- `MQTT_BROKER_BRIDGE_PASSWORD` - The password for the broker bridge user. (***Keep this value private***.) +`MQTT_BROKER_BRIDGE_USERNAME` - The username used to authenticate with the external MQTT broker.
+`MQTT_BROKER_BRIDGE_PASSWORD` - The password for the broker bridge user. (*Keep this value private*.) **MQTT topics** Specify one or more topics, separated by commas. ```bash copy -# Replace with your own values. MQTT_TOPICS=device/+/telemetry,device/+/status ``` `MQTT_TOPICS` - A list of MQTT topics that SpaceDF subscribes to or bridges. From e9ec74ccdd34e8cfb0d3fcaaeb0bc001b1488828 Mon Sep 17 00:00:00 2001 From: kinhdev24 Date: Thu, 29 Jan 2026 16:01:47 +0700 Subject: [PATCH 2/3] chore: Updated self hosting doc --- src/app/_components/CardInfo.jsx | 2 +- src/components/ui/badge.tsx | 48 + .../self-hosting/docker/advanced-setup.mdx | 1798 +++++++++++------ .../self-hosting/docker/quick-start.mdx | 5 +- .../getting-started/self-hosting/index.mdx | 2 +- 5 files changed, 1271 insertions(+), 584 deletions(-) create mode 100644 src/components/ui/badge.tsx diff --git a/src/app/_components/CardInfo.jsx b/src/app/_components/CardInfo.jsx index 12b2fbd..2723a87 100644 --- a/src/app/_components/CardInfo.jsx +++ b/src/app/_components/CardInfo.jsx @@ -9,7 +9,7 @@ export const CardInfo = ({ }) => { return (
{title && (

{title}

diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..ba40cc1 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + link: "text-primary underline-offset-4 [a&]:hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/src/content/getting-started/self-hosting/docker/advanced-setup.mdx b/src/content/getting-started/self-hosting/docker/advanced-setup.mdx index 18e4313..84cf060 100644 --- a/src/content/getting-started/self-hosting/docker/advanced-setup.mdx +++ b/src/content/getting-started/self-hosting/docker/advanced-setup.mdx @@ -3,6 +3,7 @@ import { Table } from 'nextra/components' import { Callout } from 'nextra/components' import { CardInfo } from "@/app/_components/CardInfo.jsx" import { Tabs } from 'nextra/components' +import { Badge } from "@/components/ui/badge.tsx" import { NumberOfContent, @@ -30,7 +31,7 @@ If you only want to get SpaceDF running quickly with default settings, see [Quic - + @@ -149,131 +150,226 @@ Before starting SpaceDF, you need to set up the environment variables. - - Review the configuration options below and make sure all secret values are set before starting SpaceDF. - + + ⚠️ **Action Required:** You must replace all sample values in the .env file before starting the app. + - -***Security notes*** -- **Do not commit** passwords, secrets, or API keys to Git -- **Do not expose secret values** on the client side (browser) -- If a value is marked as **“Keep this value secret”**, it must never be shared publicly -- **Do not reuse secrets** across systems or environments -- Use strong and unique passwords for all services -- Use the **minimum required permissions** for all credentials -- **Rotate keys immediately** if they are **exposed** -- Use **HTTPS / WSS** in production environments - + + - **Use strong passwords**: Make them long and unique for every service. + - **Use HTTPS**: Always use a secure connection (https://) when your site is live. + - **Keep it private**: If a value says "Secret", never share it with anyone. + + + + - **Do not share your `.env` file**: Never upload this file to GitHub, GitLab, or share screenshots of it. + - **Do not reuse passwords**: Don't use the same password you use for other websites. + - **Do not expose secrets**: Never use these secret keys in your frontend code (browser-side). + ### Configuring Environment Variables -This section explains how to configure the required environment variables in the `.env` file before starting SpaceDF. +Now that you have your `.env` file, you need to configure the settings. + + + Open the file Open the `.env` file using any text editor of your choice.
+ + Recommended Editors: + - **All platforms:** VS Code (Recommended for easier reading) + - **Windows:** Notepad + - **macOS:** TextEdit + - **Linux:** Nano or Vim +
+ + **Update the values** Review the sections below and update the corresponding values in your file. + + + + - Required These values are mandatory. SpaceDF **will not start** without them. + - Optional These are for advanced features. You can leave them as default or skip them for now + - Coming Soon Features planned for future updates. **No action required** at this time. + + +#### Backend Services -Open the `.env` file using a text editor (for example: VS Code, Nano, or Notepad). +This section configures the connections between SpaceDF and your infrastructure (such as your database and cache). -#### Services Configuration +These settings are the backbone of your server. -These environment variables configure the backend services that power the self-hosted SpaceDF server. -Backend services include APIs, messaging, databases, and integrations required to run the core system. +##### RabbitMQ Configuration +Required -##### RabbitMQ credentials -> RabbitMQ is used by SpaceDF for **background tasks** and **internal message** processing between services. +SpaceDF uses RabbitMQ to handle background tasks and internal messaging.
+You need to set up credentials so the system can connect to the message queue. -You can **choose any username and password you want**, as long as: -- The same values are used by **RabbitMQ** and **all SpaceDF services** -- The credentials are defined in the environment variables below +**Set your credentials** You can choose any username and password you want. ```bash copy -# Replace with your own values. -RABBITMQ_DEFAULT_USER=your_username -RABBITMQ_DEFAULT_PASS=your_password +RABBITMQ_DEFAULT_USER="your_secure_username" +RABBITMQ_DEFAULT_PASS="your_secure_password" ``` -`RABBITMQ_DEFAULT_USER` - The username SpaceDF uses to connect to RabbitMQ.
-`RABBITMQ_DEFAULT_PASS` - The password for the RabbitMQ user above. - - - Do not use simple or common passwords. This account controls access to your message queue.
- Keep these credentials safe—you may need them to log in to RabbitMQ for troubleshooting later. -
+**Variable Details:** +- `RABBITMQ_DEFAULT_USER`: The username for the connection. +- `RABBITMQ_DEFAULT_PASS`: The password for the connection. + + - **Consistency:** If you are running RabbitMQ via Docker or another service, these values must match the credentials defined there. + - **Security:** Use a strong password. You may need these credentials later to log in to the RabbitMQ dashboard for troubleshooting + --- ##### Authentication (JWT) -> SpaceDF uses JSON Web Tokens (JWT) to authenticate users and secure API requests. +Required -You must set a **private key** and a **public key** before starting SpaceDF. +SpaceDF uses **JSON Web Tokens (JWT)** to secure API requests. You need to generate a specific key pair for this to work. - **Recommended:** Generate a new key pair + **Generate & Copy Keys**
+ Choose your operating system below to generate and copy the keys. + + + + **1. Generate Keys** + Open Terminal and run: + ```bash copy + openssl genrsa -out private_key.pem 2048 + openssl rsa -in private_key.pem -pubout -out public_key.pem + ``` + + **2. Copy to Clipboard** + Run these commands to copy the keys directly (no manual selection needed): + ```bash copy + # Copy Private Key + pbcopy < private_key.pem + + # Copy Public Key + pbcopy < public_key.pem + ``` + + + + > **Note:** Please use **Git Bash** for these commands. Do not use PowerShell or CMD. + + **1. Generate Keys** + Run this in Git Bash: + ```bash copy + openssl genrsa -out private_key.pem 2048 + openssl rsa -in private_key.pem -pubout -out public_key.pem + ``` + + **2. Copy to Clipboard** + Run these commands to copy the keys to your Windows clipboard: + ```bash copy + # Copy Private Key + cat private_key.pem | clip + + # Copy Public Key + cat public_key.pem | clip + ``` + + + + **1. Generate Keys** + Open Terminal and run: + ```bash copy + openssl genrsa -out private_key.pem 2048 + openssl rsa -in private_key.pem -pubout -out public_key.pem + ``` + + **2. Copy Content** + Run these commands to view the key content, then manually select and copy it: + ```bash copy + # Show Private Key -> Copy the output + cat private_key.pem + + # Show Public Key -> Copy the output + cat public_key.pem + ``` + +
-```bash copy -openssl genrsa -out private_key.pem 2048 -openssl rsa -in private_key.pem -pubout -out public_key.pem -``` + - Copy the key contents from your local machine
- Choose the command based on your operating system: - - - ```bash copy - pbcopy < private_key.pem - pbcopy < public_key.pem - ``` - + **Update `.env` file**
+ Paste the copied keys into your `.env` file. +
- - ```powershell copy - Get-Content private_key.pem | Set-Clipboard - Get-Content public_key.pem | Set-Clipboard - ``` - + + ⚠️ **Formatting Rule:** Because these keys span multiple lines, you **must** enclose the entire value in double quotes (`""`). If you omit the quotes, the server will fail to start. + - - ```bash copy - cat private_key.pem | xclip -selection clipboard - cat public_key.pem | xclip -selection clipboard - ``` - - - - - Paste the keys into your `.env` file: - -```bash copy -JWT_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----... -JWT_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----... -``` +**Example of correct formatting:** -`JWT_PRIVATE_KEY` - Signs authentication tokens (***Keep this key secret.***).
-`JWT_PUBLIC_KEY` - Verifies authentication tokens. This key can be shared with other services if needed. -> Make sure the keys are pasted correctly and not broken across lines. +```bash +# ✅ CORRECT (Quotes are used) +JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA... +... (your long key content) ... +...O4zXk= +-----END RSA PRIVATE KEY-----" + +JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG... +... (your long key content) ... +...IDAQAB +-----END PUBLIC KEY-----" +``` --- ##### Google OAuth -> Google OAuth allows users to sign in to SpaceDF using their Google account. +Optional -To enable Google login, you need to create OAuth credentials in the ***Google Cloud Console*** and set the values below in your `.env` file. +Enable this to allow users to sign in to SpaceDF using their Google accounts. -**How to get Google OAuth credentials** - - Go to the [Google Cloud Console](https://console.cloud.google.com/) +You will need to generate credentials in the `Google Cloud Console`. + +**1. Get Credentials** + +Follow these steps to create your `Client ID` and `Secret`. + + + Go to the [Google Cloud Console](https://console.cloud.google.com/)
+ Create a new project (or select an existing one).
- Create or select a project. + Configure Consent Screen
+ - Go to **APIs & Services** > **OAuth consent screen**.
+ - Select **External** and click **Create**.
+ - Fill in the required App Information (App name, User support email) and click Save.
+ *(You can skip the Scopes and Test Users sections for now)*.
- Enable **Google Identity Services**. - - - Go to **APIs & Services → Credentials**. - - - Create an **OAuth 2.0 Client ID:**
- Application type: **Web application**
+ Create Credentials + - Go to **APIs & Services** > **Credentials**. + - Click + **Create Credentials** and select **OAuth client ID**. + - **Application type:** Select **Web application**. + - **Name:** Enter a name (e.g., "SpaceDF Self-Hosted"). - *If you see a warning about **OAuth consent screen** or **branding** not being configured, you must complete the **OAuth consent screen setup** first before creating the Client ID.*
- - Authorized redirect URI: (`GOOGLE_CALLBACK_URL`)
- This is the callback URL that Google redirects to after a user successfully signs in with Google. + + **Set Redirect URI (Important)** Scroll down to **Authorized redirect URIs** and click **Add URI**.
+ Enter the URL based on your environment: ```bash copy # Production https://your-domain.com/auth/google/callback @@ -281,468 +377,672 @@ To enable Google login, you need to create OAuth credentials in the ***Google Cl # Development http://localhost:3000/auth/google/callback ``` + > Note: This URL must match exactly what you put in the .env file later.
- - Copy the generated **Client ID** and **Client Secret** into your `.env` file. + + Copy **Keys** Click **Create**. A popup will appear with your **Client ID** and **Client Secret**. Copy these values. +**2. Update Environment** + +Open your `.env` file and paste the values you just obtained. + +**Connection Settings** + ```bash copy -# Replace with your own values. -GOOGLE_CALLBACK_URL=your_google_callback_uri -GOOGLE_CLIENT_ID=your_google_client_id #(Step 5) -GOOGLE_CLIENT_SECRET=your_google_client_secret #(Step 5) +# 1. The URL you configured in Step 4 above +GOOGLE_CALLBACK_URL="http://localhost:3000/auth/google/callback" + +# 2. The Client ID from Step 5 +GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com" + +# 3. The Client Secret from Step 5 +GOOGLE_CLIENT_SECRET="your-client-secret" ``` -`GOOGLE_CALLBACK_URL` - The URL Google redirects users back to after successful login.
-`GOOGLE_CLIENT_ID` - Identifies your application to Google.
-`GOOGLE_CLIENT_SECRET` - A private key used by SpaceDF to securely communicate with Google (***Keep this value secret.***) - -Use HTTPS for the callback URL in production. - +**Variable Details**
+- `GOOGLE_CALLBACK_URL` - Where Google sends the user after login. **Must match** the URI in Google Cloud Console exactly.
+- `GOOGLE_CLIENT_ID` - The public identifier for your app.
+- `GOOGLE_CLIENT_SECRET` - The secret key to authenticate your app. **Keep this private**. --- -##### Apple OAuth (Coming Soon) +##### Apple OAuth +Coming Soon -Apple sign-in support is planned but not yet supported in SpaceDF. +Support for "Sign in with Apple" is currently in development. It is **not enabled in this version** of SpaceDF. - - Do not configure these values yet. - Apple OAuth is **not supported** in the current release. - + + ℹ️ **No Action Required:** You can safely skip this section. Please leave these values blank or commented out in your `.env` file. + ```bash copy -# Apple OAuth (reserved for future use) -APPLE_CLIENT_ID=your_apple_client_id -APPLE_CLIENT_SECRET=your_apple_client_secrect -APPLE_CLIENT_KEY=your_apple_client_key -APPLE_CERTIFICATE_KEY=your_apple_certificate_key +# Apple OAuth (Reserved for future updates) +# Leave these lines commented out for now: +# APPLE_CLIENT_ID= +# APPLE_CLIENT_SECRET= +# APPLE_CLIENT_KEY= +# APPLE_CERTIFICATE_KEY= ``` --- ##### Auth Service -> The Auth Service is responsible for user authentication, authorization, and tenant management in SpaceDF. +Required -**`AUTH_POSTGRES_PASSWORD`** +The Auth Service manages user logins and security. You need to configure its database connection and domain settings. -The password used by the Auth Service to connect to its *PostgreSQL database*. (*Use a strong and unique password.*) +**1. Security Credentials** -```bash copy -AUTH_POSTGRES_PASSWORD=your_auth_postgres_password -``` +Set up the password for the database and the encryption key for sessions. -**`AUTH_SECRET_KEY`** + + **Generate a Secret Key**: Run this command in your terminal to get a secure random string: -A secret key used to sign and validate authentication-related data. (*Keep this value private.*) + ```bash copy + openssl rand -hex 32 + ``` + -Generate secure random values for secret keys: -```bash copy -openssl rand -hex 32 -``` + + **Update `.env`**: Copy the generated string and your database password into the file: + + ```bash copy + # 1. Database Password (Make sure it is strong) + AUTH_POSTGRES_PASSWORD=your_secure_db_password -Paste the generated Secret Key into your `.env` file: -```bash copy -AUTH_SECRET_KEY=your_auth_secret_key -``` + # 2. Secret Key (Paste the output from Step 1) + AUTH_SECRET_KEY=paste_generated_key_here + ``` + -**`DEFAULT_TENANT_HOST`** +**2. Tenant Configuration** -The default domain or host assigned to the initial tenant. This is usually your main application domain. +This defines the main domain where your application runs. + + + ⚠️ **Format Note:** Do not include `http://` or `https://`. Only enter the domain name + + For Localhost: Use this if you are running SpaceDF on your own computer. ```bash copy DEFAULT_TENANT_HOST=localhost ``` + For Live Server: + Use your actual domain name (e.g., `app.spacedf.com` or `spacedf.com`). ```bash copy - DEFAULT_TENANT_HOST=your_api_domain + DEFAULT_TENANT_HOST=your-domain.com ``` - --- ##### S3 Service -> The Amazon S3 service is used by SpaceDF to store files such as uploads, assets, and generated data. +Required - - SpaceDF supports **Amazon S3 only** for file storage in this setup. - +SpaceDF uses Amazon S3 to store uploaded files, assets, and generated data. -SpaceDF connects to Amazon S3 using **IAM credentials** that you provide in the `.env` file. All file operations (upload, read, delete) are handled internally by SpaceDF services.
-End users do **not** need AWS accounts or credentials. +You need to provide **IAM Credentials** so SpaceDF can access your storage securely. -If you do not already have an AWS account, create one at, [see more](https://repost.aws/knowledge-center/create-and-activate-aws-account):
+ + ℹ️ **Note:** Currently, SpaceDF supports **Amazon S3 only**.
+ Other providers (like Google Cloud Storage or MinIO) are not yet supported. +
+ +If you do not already have an AWS account, create one at:
https://signin.aws.amazon.com/signup?request_type=register -After creating your account, sign in to the AWS Management Console to continue with the steps below. +**1. Create an S3 Bucket** -**Create an S3 bucket** +This is the "container" where your files will be stored. - Open the [Amazon S3 Console](https://console.aws.amazon.com/s3/) + Log in to your AWS account and navigate to the [Amazon S3 Console](https://console.aws.amazon.com/s3/) - Create a new bucket + Create a new bucket
+ Click the orange `Create bucket` button. Configure the following: + - **Bucket name:** Enter a globally unique name (e.g., `spacedf-storage-yourname`). + - **AWS Region:** Choose a region close to your users (e.g., `Sydney (ap-southeast-2)`) + - **Block Public Access:** Keep "Block all public access" **checked** (Enabled) for security. SpaceDF handles access control internally. +
- Choose:
- A unique bucket name -> will be used for `AWS_STORAGE_BUCKET_NAME`
- An AWS region close to your server → will be used for `AWS_REGION` - + Finish
+ Scroll to the bottom and click `Create bucket`. Note down your **Bucket Name** and **Region Code**.
-Save the bucket name and region for later use. +Refer: [Official AWS Guide on Creating Buckets](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) + +**2. Create IAM Credentials** -Read more: [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/GetStartedWithS3.html#creating-bucket) +Now you need to create a "Service User" that has permission to read/write to that bucket. **Create IAM credentials** - Open the [AWS IAM Console](https://console.aws.amazon.com/iam/) + Open the [AWS IAM Console](https://console.aws.amazon.com/iam/) and click **Users** in the left sidebar. - Create a new **IAM user** + **Add User**
+ Click **Create user**. + * **User name:** Enter a name like `spacedf-s3-user`. + * Click **Next**.
- Grant the user access to the S3 bucket
- *Recommended:* grant only required S3 permissions (read/write) + **Set Permissions** + - Select **Attach policies directly**. + - In the search bar, type `AmazonS3FullAccess`. + - Check the box next to `AmazonS3FullAccess` + - Click **Next**, then click **Create user** + > Note: For advanced security, you can create a custom policy restricted to a single bucket later
- Create an **Access Key** for the user
- Access Key ID, Secret Access Key + **Generate Keys** + - Click on the user name you just created. + - Go to the **Security credentials** tab + - Scroll down to **Access keys** and click **Create access key**. + - Select **Application running outside AWS** + - Click **Next** -> Click **Create access key**.
-Read more: [AWS documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) + + ⚠️ **Important:** This is the **only time** you can view the **Secret access key**. Copy it immediately or download the `.csv` file + + +Refer: [Managing Access Keys in IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) - -Use IAM policies with **minimum required permissions**. - +**3. Update Environment** + +Open your `.env` file and fill in the values from the previous steps. -**Set values in `.env`** ```bash copy -# Replace with your own values. -AWS_ACCESS_KEY_ID=your_access_key_id #IAM credentials (Step 4) -AWS_SECRET_ACCESS_KEY=your_secret_access_key #IAM credentials (Step 4) - -AWS_STORAGE_BUCKET_NAME=your_aws_storage_bucket_name #S3 bucket (Step 3) -AWS_REGION=your_aws_region #S3 bucket (Step 3) +# AWS S3 Configuration +AWS_ACCESS_KEY_ID="your_access_key_id_from_phase_2" +AWS_SECRET_ACCESS_KEY="your_secret_key_from_phase_2" + +AWS_STORAGE_BUCKET_NAME="your_bucket_name_from_phase_1" +AWS_REGION="your_region_code" ``` -- `AWS_ACCESS_KEY_ID` - The access key used by SpaceDF to authenticate with S3. -- `AWS_SECRET_ACCESS_KEY` - The secret key paired with the access key above. (*Keep this value private*) -- `AWS_STORAGE_BUCKET_NAME` - The name of the S3 bucket where SpaceDF stores files. -- `AWS_REGION` - The AWS region where the S3 bucket is located. (e.g., `us-east-1`, `ap-southeast-1`). +**Variable Details:** +- `AWS_ACCESS_KEY_ID`: The ID connecting to your IAM user. +- `AWS_SECRET_ACCESS_KEY`: The password for the IAM user. +- `AWS_STORAGE_BUCKET_NAME`: The name of your bucket (e.g., `my-app-storage`). +- `AWS_REGION`: The code for your region. --- ##### Redis -> Redis is used by SpaceDF for caching and fast data access. +Required -Set the Redis connection URL in the `.env` file. +SpaceDF uses Redis for caching data and handling background tasks (like sending emails). + +**1. Default Configuration (Docker)** + +If you are installing SpaceDF using the standard Docker setup, you **do not need to change this**. ```bash copy -# Replace with your own values. REDIS_HOST="redis://redis:6379/1" ``` -- `redis://` — Connection protocol -- `redis` — Redis service name (default in Docker) -- `6379` — Default Redis port -- `/1` — Redis database number - - - - You are using the provided Docker setup - - Redis is running as part of the included Docker Compose file + ❓ **Why** In our Docker setup, the service is named `redis`. SpaceDF automatically finds it at this address. - - - Redis runs on a different server or host - - Redis uses a non-default port - - You want to use a different Redis database + ℹ️ **Syntax with Password:** If your Redis requires a password, follow this format:
+ `redis://:PASSWORD@HOST:PORT/DB_INDEX`
---- -##### Dashboard Service -> The Dashboard Service provides the web interface for managing SpaceDF, including administration and monitoring features. - -**`DASHBOARD_POSTGRES_PASSWORD`** - -The password used by the Dashboard Service to connect to its PostgreSQL database. (*Use a strong and unique password*.) - +Example: ```bash copy -DASHBOARD_POSTGRES_PASSWORD=your_postgres_password +# Example for external Redis with password +REDIS_HOST="redis://:mypassword123@192.168.1.50:6379/0" ``` -**`DASHBOARD_SECRET_KEY`** +**3. Connection Details** -A secret key used to sign and validate authentication-related data. (*Keep this value private.*) - -Generate secure random values for secret keys: -```bash -openssl rand -hex 32 -``` - -Paste the generated Secret Key into your `.env` file: -```bash copy -DASHBOARD_SECRET_KEY=your_secret_key -``` +Breakdown of the connection string components: +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Component + + Description + + Example +
`redis://`ProtocolAlways `redis://`
`:password@`(Optional) Password prefix`:secret123@`
`host`Server IP or Domain`localhost` or `192.168.x.x`
`:6379`Port numberDefault is `6379`
`/1`Database Index (0-15)`/1`
+
--- -##### Device Service -> The Device Service manages devices, device data, and communication with telemetry-related services in SpaceDF. +#### Service Credentials +Required -**`DEVICE_POSTGRES_PASSWORD`** +The following services (Dashboard & Device) operate independently. Each requires its own Database Password and unique Secret Key to function securely. -The password used by the Device Service to connect to its PostgreSQL database. (*Use a strong and unique password*.) + + 🔐 **Security Rule** Do **not reuse** the same password or secret key across different services.
+ Generate a unique value for each variable below. +
+**1. Generate Secret Keys** Run this command twice to generate two different keys: ```bash copy -DEVICE_POSTGRES_PASSWORD=your_postgres_password -``` - -**`DEVICE_SECRET_KEY`** - -A secret key used to sign and validate authentication-related data. (*Keep this value private.*) - -Generate secure random values for secret keys: -```bash openssl rand -hex 32 ``` -Paste the generated Secret Key into your `.env` file: +**2. Update `.env`** Update the values for both the Dashboard and Device services. ```bash copy -DEVICE_SECRET_KEY=your_secret_key +# --- Dashboard Service --- +# Password for Dashboard Database +DASHBOARD_POSTGRES_PASSWORD=your_dashboard_db_password +# Secret Key for Dashboard (Paste generated key 1) +DASHBOARD_SECRET_KEY=your_dashboard_secret_key + +# --- Device Service --- +# Password for Device Database +DEVICE_POSTGRES_PASSWORD=your_device_db_password +# Secret Key for Device (Paste generated key 2) +DEVICE_SECRET_KEY=your_device_secret_key ``` --- ##### Telemetry service +Required -Defines the internal Docker service address that SpaceDF uses to communicate with the Telemetry Service. +This variable points to the internal Docker container that handles device data. -When using the **default Docker setup**, the Telemetry Service runs as a Docker container and is accessible via its **service name**. +**Configuration** In 99% of cases (Local & Production), you **should keep the default value**. ```bash copy # Default (recommended) TELEMETRY_SERVICE_URL=http://telemetry:8080 ``` + - - This value is the same for both **local and production** deployments when using Docker Compose. - - The Telemetry Service runs from a **Docker image** and is exposed internally. - - You **do not need to change this value** in most cases. + title: "!text-base", + description: "!text-sm" +}} + title="❓ Why" +> + - **No Action Needed:** This service runs automatically inside Docker. + - **Internal Access:** SpaceDF uses this URL to talk to the service internally. It does not need a public domain or HTTPS. -Only change this value if you modify the Telemetry Service name or port in `docker-compose.yml`. +**When to change this?** + +Only modify this value if you have manually customized the `docker-compose.yml` file to run the Telemetry service on a different port or server. --- ##### EMQX Service -> EMQX is the MQTT broker used by SpaceDF to handle device messaging and real-time communication. +Required -Choose a username and a strong password: +EMQX is the core engine that handles real-time messages from your devices. SpaceDF needs to connect to the EMQX Management API to control it. +**1. API Connection (Internal)** Use the default value if you are running SpaceDF via Docker. ```bash copy -# Replace with your own values. EMQX_HOST=http://emqx:18083/api/v5 -EMQX_USERNAME=your_emqx_username -EMQX_PASSWORD=your_emqx_password ``` -`EMQX_HOST` - The base URL of the EMQX Management API used by SpaceDF. This is typically the EMQX service running inside Docker.
-`EMQX_USERNAME` - The username SpaceDF uses to authenticate with the EMQX broker.
-`EMQX_PASSWORD` - The password for the EMQX user above. (*Use a strong and unique password*.) + +**2. Admin Credentials** Define the username and password SpaceDF will use to log in to EMQX. +```bash copy +EMQX_USERNAME=your_secure_username +EMQX_PASSWORD=your_secure_password +``` + + + ⚠️ **Consistency:** These credentials must match the admin user defined in your EMQX configuration (or Dashboard). + --- ##### Broker Bridge Service -> The Broker Bridge Service connects SpaceDF to an external MQTT broker or bridges messages between brokers. +Required + +The Bridge Service acts as a listener. It connects to the MQTT Broker to receive device data and forward it to SpaceDF. + +**1. Broker Authentication** -**Broker credentials** +Enter the credentials required to connect to the MQTT Broker (usually the same credentials used for EMQX clients). ```bash copy -# Replace with your own values. -MQTT_BROKER_BRIDGE_USERNAME=your_username -MQTT_BROKER_BRIDGE_PASSWORD=your_password +MQTT_BROKER_BRIDGE_USERNAME=your_bridge_username +MQTT_BROKER_BRIDGE_PASSWORD=your_bridge_password ``` -`MQTT_BROKER_BRIDGE_USERNAME` - The username used to authenticate with the external MQTT broker.
-`MQTT_BROKER_BRIDGE_PASSWORD` - The password for the broker bridge user. (*Keep this value private*.) +**2. Topic Subscription** -**MQTT topics** +Define which MQTT topics SpaceDF should listen to. -Specify one or more topics, separated by commas. ```bash copy MQTT_TOPICS=device/+/telemetry,device/+/status ``` -`MQTT_TOPICS` - A list of MQTT topics that SpaceDF subscribes to or bridges. + +**Syntax Guide:** +- Comma (`,`): Use to separate multiple topics. +- Plus (`+`): Wildcard for a single level (e.g., matching any device ID). +- Hash (`#`): Wildcard for all remaining levels. + + + 📝 **Example:** `device/+/telemetry` will match `device/sensor-01/telemetry` and `device/sensor-02/telemetry`. + --- -##### AWS to access to SES service -> SpaceDF uses email services to send system emails such as account verification, password resets, and notifications. +##### Email Service (AWS SES) +Required -This setup commonly uses **AWS Simple Email Service (SES)**, but can be adapted to other SMTP-compatible providers. +SpaceDF uses AWS SES (Simple Email Service) to send account verification emails, password resets, and notifications. -**How to get AWS SES credentials** - - Sign in to the [AWS Console](https://aws.amazon.com/console/) - - - Open **Simple Email Service (SES)**. +**1. Setup & Verify** + +Before sending emails, you must tell AWS which email address or domain you own. + + + **Go to SES Console**
+ Log in to AWS and open the [Amazon SES Console](https://console.aws.amazon.com/ses/).
- - Verify your domain or sender email address. + + +**Verify Identity** +* Go to **Configuration** -> **Verified identities**. +* Click **Create identity**. +* Choose **Email address** (easiest) or **Domain**. +* Check your inbox and click the verification link sent by AWS. - - Create **SMTP credentials** in SES: -
    -
  • - These are different from your AWS access keys. -
  • -
+ +Refer: [AWS Guide on Verifying Identities](https://docs.aws.amazon.com/ses/latest/dg/creating-identities.html) + +**2. Get SMTP Credentials** + + + ⚠️ **Crucial Distinction:** Do **not** use your standard AWS Access Keys.
+ You must generate specific **SMTP Credentials**. +
+ + +**Create Credentials** +* Go to **SMTP Settings** in the left sidebar. +* Click the button **Create SMTP credentials**. - - Copy the SMTP username and password into: -
    -
  • - `EMAIL_HOST_USER` -
  • -
  • - `EMAIL_HOST_PASSWORD` -
  • -
+ + +**Copy Keys** +* Click **Create**. +* Download the credentials or copy the **SMTP Username** and **SMTP Password** immediately. -Set the following values in your `.env` file. +Refer: [AWS Guide on Obtaining SMTP Credentials](https://docs.aws.amazon.com/ses/latest/dg/smtp-credentials.html) + +**3. Update Environment** + +Update the `.env` file with the SMTP credentials you just created. ```bash copy -# Replace with your own values. -EMAIL_HOST_USER=AKIAXXXXXXXX -EMAIL_HOST_PASSWORD=XXXXXXXXXXXXXXXX -DEFAULT_FROM_EMAIL=no-reply@spacedf.example +# Acount AWS to access to SES service +EMAIL_HOST_USER=AKIA... #(Your SMTP Username) +EMAIL_HOST_PASSWORD=BAj... #(Your SMTP Password) +DEFAULT_FROM_EMAIL="no-reply@yourdomain.com" ``` -- `EMAIL_HOST_USER` - The SMTP username generated by AWS SES. -- `EMAIL_HOST_PASSWORD` - The SMTP password generated by AWS SES (***Keep this value secret***) -- `DEFAULT_FROM_EMAIL` - The sender email address shown to users. + +**Variable Details:** +- `EMAIL_HOST_USER`: The username specifically generated for SMTP (starts with `AKIA...` usually). +- `EMAIL_HOST_PASSWORD`: The long secret string for SMTP authentication. +- `DEFAULT_FROM_EMAIL`: The email address that will appear as the sender (Must be verified in Phase 1). + --- ##### MPA Service -> The MPA Service connects SpaceDF to an MQTT broker to receive and publish messages for application-level processing. +Required + +The MPA Service connects to the MQTT Broker to process application events. It acts as a client that listens for device messages. + +**Configuration** + +Update the `.env` file with the connection details below. -Example: MQTT broker running in Docker ```bash copy -# Replace with your own values. -MQTT_BROKER=emqxl +# --- Connection Settings --- +# Hostname (Use 'emqx' if running inside Docker) +MQTT_BROKER=emqx +# Port (1883 for TCP, 8883 for SSL/TLS) MQTT_PORT=1883 -MQTT_USERNAME=mpa -MQTT_PASSWORD=change_this_to_a_secure_password -MQTT_CLIENT_ID=spacedf-mpa +# Unique name for this client +MQTT_CLIENT_ID=spacedf-mpa-client +# Topics to subscribe to (use '+' for wildcards) MQTT_TOPIC=devices/+/events -``` -- `MQTT_BROKER` - The hostname or IP address of the MQTT broker. -- `MQTT_USERNAME` - The username used to authenticate with the MQTT broker. -- `MQTT_PASSWORD` - The password for the MQTT user. (***Keep this value private***.) -- `MQTT_PORT` - The port used to connect to the MQTT broker (e.g., `1883` for plain TCP, `8883` for TLS). -- `MQTT_CLIENT_ID` - A unique client identifier for the MPA Service when connecting to MQTT. -- `MQTT_TOPIC` - The MQTT topic the MPA Service subscribes to. - - - Use TLS (`8883`) in production if available
- Restrict broker permissions to required topics only -
---- -##### Bootstrap Service -> The Bootstrap Service is the **backend service** responsible for initial system setup and cross-service coordination when SpaceDF starts. - -This service runs inside Docker as the SpaceDF backend. -**Service host** - -This value defines the **base URL** where the Bootstrap (Backend) service is accessible. -```bash copy -# Local development (default) -HOST=http://localhost:8000 -``` -For production deployments, set this to your **public API domain**: -```bash copy -# Replace with your public API domain. -HOST=https://api.spacedf.example +# --- Authentication --- +MQTT_USERNAME=mpa_user +MQTT_PASSWORD=change_this_to_secure_password ``` -`HOST` - The public base URL where the Bootstrap Service is accessible. This is used by other services to communicate with it. +**Variable Details** +- `MQTT_BROKER`: The IP or Hostname of the broker. +- `MQTT_PORT`: `1883` (Plain) or `8883` (Secure). +- `MQTT_TOPIC`: The data channel to listen to. +- `MQTT_USERNAME`: Must match a user created in EMQX. +- `MQTT_PASSWORD`: The password for the user above. - - - HOST in your `.env` file - - The backend port in `docker-compose.yml` +}} + title="⚠️ Production Tips:" +> + - **Use TLS:** In a live environment, change the port to 8883 for encrypted connections. + - **Client ID:** Ensure `MQTT_CLIENT_ID` is unique. If you run multiple instances of SpaceDF, they cannot share the same ID +--- +##### Bootstrap Service +Required + +The Bootstrap Service is the **main backend API of SpaceDF**. It orchestrates the system setup and handles requests from your frontend applications. + +**1. API URL (`HOST`)** + +This variable defines the public address where your Backend API is accessible. + + + + For Localhost: Use this default value if you are running SpaceDF on your computer. + ```bash copy + HOST=http://localhost:8000 + ``` + + + + For Public Domain: Set this to your actual API domain. + ```bash copy + HOST=https://api.yourdomain.com + ``` + + + ⚠️ **Requirement:** In production, you should use HTTPS for security. . + + + -**CORS origins** +**2. Access Control (`CORS`)** -List all frontend origins that should be allowed to access the service. -Separate multiple values with commas. +This setting controls which websites (frontends) are allowed to talk to your backend. -Development (local) -```bash copy -# Local development (default) -CORS_ALLOWED_ORIGINS=http://localhost,http://localhost:3000,http://localhost:3001 -``` +This prevents unauthorized websites from using your API. -Production -```bash copy -# Replace with your own values. -CORS_ALLOWED_ORIGINS=https://app.spacedf.example,https://admin.spacedf.example -``` + + - List all frontend URLs exactly (including `http/https` and port). + - Separate multiple URLs with a comma (`,`) and `no spaces`. + -`CORS_ALLOWED_ORIGINS` - A comma-separated list of allowed origins for cross-origin requests. This controls which frontend domains can access the Bootstrap Service. + + + **Allow Local Ports:** List all the local ports your frontend apps use (e.g., Dashboard on `3000`, Admin portal on `3001`). + ```bash copy + CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001 + ``` + - - **Important notes** - - Controls **which frontend domains can access** the Bootstrap service - - Each frontend **must be listed explicitly**, including protocol and port - - If you change frontend ports in `docker-compose.yml`, update both the Docker ports and `CORS_ALLOWED_ORIGINS` - + + **Allow Public Domains:** List your actual frontend domains (App and Admin). + ```bash copy + CORS_ALLOWED_ORIGINS=https://app.yourdomain.com,https://admin.yourdomain.com + ``` + + -Example: + + - If you change a frontend port (e.g., from `3000` to `4000`) in `docker-compose.yml`, you must update this list to include `http://localhost:4000` + - If you forget, the app will show a `Network Error` or `CORS-ERROR`. + -If the Dashboard is moved from port `3000` to `4000`, you must add: +**3. Security Key** -```text copy -http://localhost:4000 -``` -to `CORS_ALLOWED_ORIGINS`. +This key secures the internal operations of the backend. -**Secret key**: Generate a secure random value: -```bash + + **Generate Key** + ```bash copy openssl rand -hex 32 -``` - -Set it in your `.env` file: + ``` + -```bash copy -# Replace with your own values. -BOOTSTRAP_SECRET_KEY=generated_secret_value -``` -`BOOTSTRAP_SECRET_KEY` - A secret key used to secure internal Bootstrap operations. (***Keep this value private.***) + + **Update `.env`** + ```bash copy +BOOTSTRAP_SECRET_KEY=paste_your_generated_key_here + ``` + --- ##### Organization Initialization -> These settings are used to create the **initial organization and owner account** when SpaceDF starts for the first time. This step runs only during the first startup. +Required + +These settings are used to generate the **Super Admin account** and the **Default Organization** when SpaceDF runs for the **very first time**. + +**1. Setup Credentials** + +Define your organization name and the login credentials for the owner. ```bash copy # Replace with your own values. -ORG_NAME=SpaceDF -ORG_SLUG=spacedf -OWNER_EMAIL=admin@spacedf.example -OWNER_PASSWORD=change_this_to_a_secure_password +ORG_NAME="My IoT Company" +OWNER_EMAIL="admin@your-domain.com" +OWNER_PASSWORD="change_this_to_a_secure_password" ``` -- `ORG_NAME` - The display name of your organization. -- `ORG_SLUG` - A short, URL-friendly identifier for the organization (lowercase, no spaces). -- `OWNER_EMAIL` - The email address of the initial organization owner. -- `OWNER_PASSWORD` - The password for the owner account. (***Use a strong and secure password***.) +**2. Variable Details** +- `ORG_NAME` - The display name of your organization (e.g., "SmartHome Solutions") +- `OWNER_EMAIL` - The email address used to log in as the System Administrator. +- `OWNER_PASSWORD` - The master password for the admin account. **Must be strong**. + + + These `.env` values are only read during the initial installation. + * **To change Password/Email:** Log in to the Dashboard and go to Profile Settings. + * **To change Organization Name:** Log in to the Dashboard and go to **Organization Settings**.
+ (Modifying the `.env` file after installation will have no effect). +
+ + + ⭐ **Branding:** The `ORG_NAME` helps brand your system immediately. In production, you can point your **custom domain** (e.g., `iot.your-brand.com`) to the server to fully white-label the platform. + --- 🎉 You have now finished configuring the backend services.
@@ -750,220 +1050,476 @@ Next, continue with the frontend (web app) configuration. --- ### Admin Portal Configuration + +The Admin Portal is your central command center. It allows **Platform Administrators** to manage organizations, users, and system-wide settings. + +*(Note: This interface is for you and your staff, not for your end-users)*. + - The Admin Portal is used for **system-level** and **organization-level** administration.
- This application is typically accessed by **platform administrators**, not end users. + container: "!mt-6 !p-3 shadow-none", + title: "!text-base", + description: "!text-sm" + }} + > + SpaceDF uses `NextAuth.js` for its authentication layer. If you need to customize behavior or debug specific issues, please refer to the official documentation: + - [NextAuth.js Documentation](https://next-auth.js.org/) - General guide and concepts. + - [Environment Variables](https://next-auth.js.org/configuration/options) - Detailed list of all available options. + - [Security Guide](https://next-auth.js.org/security) - Best practices for securing your authentication.
-Configure these environment variables to connect the Admin Portal to backend services and authentication providers. - - - **Before continuing**
- Make sure you have completed the **Backend Services Configuration** above.
- The Admin Portal depends on backend authentication and API services. -
-##### Authentication + + ⚠️ **Prerequisite Check:** Please ensure you have fully configured the **Backend Services** section above.
+ The Admin Portal cannot function without a running backend. +
-**Generate NextAuth secret** +##### Authentication & URLs +Required -Open the [following link](https://generate-secret.vercel.app/32) in your browser to generate a secure random secret: +This section secures user sessions and defines the public address of your Admin Portal. -Copy the generated value and set it in your `.env` file: +**1. Generate Secret Key** Create a unique key to encrypt login sessions. Run this command in your terminal: ```bash copy -# Replace with your own values. -PORTAL_NEXTAUTH_SECRET=4f7c2a9e8d1b6c3f0a... +openssl rand -hex 32 ``` -`PORTAL_NEXTAUTH_SECRET` - A secret key used by NextAuth to encrypt sessions and tokens. (***Keep this value private.***) -**Admin Portal URL** +Update the value in your `.env` file: -Development ```bash copy -HOST_FRONTEND_ADMIN=http://localhost:3001 -PORTAL_NEXTAUTH_URL=http://localhost:3001 +# Replace with the generated string +PORTAL_NEXTAUTH_SECRET=your_generated_secret_key ``` -Production -```bash copy -HOST_FRONTEND_ADMIN=https://admin.spacedf.example -PORTAL_NEXTAUTH_URL=https://admin.spacedf.example -``` +**2. Configure Domain** Set the URL where administrators will access the portal. + + + + For Localhost: Use this setup if you are running the portal on your computer. + ```bash copy + HOST_FRONTEND_ADMIN=http://localhost:3001 + PORTAL_NEXTAUTH_URL=http://localhost:3001 + ``` + + + For Public Domain: Replace with your actual domain name. + ```bash copy + HOST_FRONTEND_ADMIN=https://admin.yourdomain.com + PORTAL_NEXTAUTH_URL=https://admin.yourdomain.com + ``` + + ⚠️ **Consistency:** In most cases, these two variables must be identical. + + + + +**Variable Details:** +- `PORTAL_NEXTAUTH_SECRET`: The encryption key for cookies/tokens. **Keep this private**. +- `HOST_FRONTEND_ADMIN`: The public URL shown in the browser address bar. +- `PORTAL_NEXTAUTH_URL`: The base URL used internally by the authentication system for callbacks. -- `HOST_FRONTEND_ADMIN` - The public URL where the Admin Portal is accessible. -- `PORTAL_NEXTAUTH_URL` - The base URL used by NextAuth for redirects and callbacks.
This value should match the Admin Portal public URL. --- -##### API Access +##### Backend Connection (API) -**Using Docker** -```bash copy -PORTAL_AUTH_API=http://haproxy:3000 -``` +The Admin Portal needs to communicate with the Backend API to handle user authentication and data retrieval. -**External API** -```bash copy -# Replace with your own values. -PORTAL_AUTH_API=https://api.spacedf.example -``` +**Choose your deployment scenario:** + + + + **Scenario: All-in-One (Docker Compose)** If you are running both the Frontend and Backend on the same server using our provided Docker setup, use the internal service name. + ```bash copy + # Connects internally via the Docker network + PORTAL_AUTH_API=http://haproxy:3000 + ``` + + ❓ **Why not localhost** Inside a Docker container, `localhost` refers to the container itself, not your computer. Using `haproxy` allows the Portal to find the API within the internal Docker network. + + + + + **Scenario: Distributed / External** Use this only if your Backend is hosted on a different server or if you are deploying the Frontend to a service like Vercel/Netlify. + ```bash copy + # Connects over the public internet + PORTAL_AUTH_API=https://api.yourdomain.com + ``` + + -`PORTAL_AUTH_API` - The backend API endpoint used by the Admin Portal for authentication.
-This typically points to the internal API gateway or load balancer. +**Variable Details:** ---- -##### References +- `PORTAL_AUTH_API` - The endpoint the Admin Portal uses to make **server-side** requests to the Backend. + + + - [NextAuth.js Documentation](https://next-auth.js.org/) - General guide and concepts. + - [Environment Variables](https://next-auth.js.org/configuration/options) - Detailed list of all available options. + - [Security Guide](https://next-auth.js.org/security) - Best practices for securing your authentication. + -Use the official documentation below if you need more details about **NextAuth** behavior or advanced configuration: -- [NextAuth.js Documentation](https://next-auth.js.org/) -- [NextAuth Environment Variables](https://next-auth.js.org/configuration/options) -- [NextAuth Security Guide](https://next-auth.js.org/security) --- ### Dashboard Configuration -This section configures the **SpaceDF Dashboard**, the main web interface used for daily operations, device monitoring, and data visualization. +The Dashboard is the primary interface for **End Users (your customers)**. It is used for daily operations, including real-time device monitoring, data visualization, and asset control. - - **Before continuing**
- Make sure you have completed the **Backend Services Configuration** above.
- The Dashboard depends on backend APIs, MQTT, and authentication services. -
+ + SpaceDF uses `NextAuth.js` for its authentication layer. If you need to customize behavior or debug specific issues, please refer to the official documentation: + - [NextAuth.js Documentation](https://next-auth.js.org/) - General guide and concepts. + - [Environment Variables](https://next-auth.js.org/configuration/options) - Detailed list of all available options. + - [Security Guide](https://next-auth.js.org/security) - Best practices for securing your authentication. + -##### Authentication + + ⚠️ **Prerequisite Check:** Please ensure you have fully configured the **Backend Services** section above.
+ The Dashboard requires the Backend API and MQTT Broker to be running to fetch data. +
-**Generate NextAuth secret** +##### Authentication & URLs +Required -Open the [following link](https://generate-secret.vercel.app/32) in your browser to generate a secure random secret: +This section secures user sessions and defines the public address of your Dashboard. -Copy the generated value and set it in your `.env` file: +**1. Generate Secret Key** Create a unique key to encrypt login sessions. Run this command in your terminal: ```bash copy -# Replace with random secret. -DASHBOARD_NEXTAUTH_SECRET=4f7c2a9e8d1b6c3f0a... - -# The public URL where the Dashboard is accessible. -DASHBOARD_NEXTAUTH_URL=http://localhost:3000 +openssl rand -hex 32 ``` -- `DASHBOARD_NEXTAUTH_SECRET` - A secret key used by NextAuth to encrypt sessions and tokens. (***Keep this value private.***) -- `DASHBOARD_NEXTAUTH_URL` - The public URL where the Dashboard is accessible.
-This value is used for redirects and callbacks. ---- -##### API Access -**Using Docker** -```bash copy -DASHBOARD_AUTH_API=http://haproxy:3000 -``` +Update the value in your `.env` file: -**External API** ```bash copy -# Replace with your own values. -DASHBOARD_AUTH_API=https://api.spacedf.example +# Replace with the generated string +DASHBOARD_NEXTAUTH_SECRET=your_generated_secret_key ``` + +**2. Configure Domain** Set the URL where end-users will access the dashboard. + + + + For Localhost: Use this setup if you are running the dashboard on your computer + ```bash copy + DASHBOARD_NEXTAUTH_URL=http://localhost:3000 + ``` + + + For Public Domain: Replace with your actual domain name. + ```bash copy + DASHBOARD_NEXTAUTH_URL=https://dashboard.yourdomain.com + ``` + + ⚠️ **HTTPS:** In production, ensure your domain is secured with SSL (HTTPS) + + + + +**Variable Details:** +- `DASHBOARD_NEXTAUTH_SECRET`: The encryption key for cookies/tokens. **Keep this private**. +- `DASHBOARD_NEXTAUTH_URL`: The public URL (Base URL) used by the authentication system for redirects and callbacks. + +--- +##### Backend Connection (API) +Required + +The Dashboard needs to communicate with the Backend API to fetch user data, device status, and charts. + +**Choose your deployment scenario:** + + + + **Scenario: All-in-One (Docker Compose)** If you are running both the Dashboard and Backend on the same server using our Docker setup, use the internal service name. + ```bash copy + # Connects internally via the Docker network + DASHBOARD_AUTH_API=http://haproxy:3000 + ``` + + ℹ️ **Network Note:** Do not use `localhost` here. Inside the Docker network, `haproxy` is the correct hostname to reach the API gateway. + + + + + **Scenario: Distributed / External** Use this **only** if your Backend is hosted on a different server or if you are deploying the Dashboard to a cloud provider (e.g., Vercel, Netlify). + ```bash copy + # Connects over the public internet + DASHBOARD_AUTH_API=https://api.yourdomain.com + ``` + + + +**Variable Details:** + +- `DASHBOARD_AUTH_API` - The endpoint the Dashboard uses for **server-side** data fetching and authentication. --- ##### Map Services -> The Dashboard uses **MapTiler** as a map service to resolve location names from coordinates and support location-based features. +Optional + +SpaceDF uses **MapTiler** to provide map visualization and **Reverse Geocoding** (converting GPS coordinates into readable addresses like "123 Main St"). + + + ℹ️ **What happens if I skip this?** The Dashboard will **still work**, but you will **not see street addresses**.
+ Locations will only be displayed as raw latitude/longitude coordinates (e.g., `10.762`, `106.660`). +
+ +**Setup Instructions:** -**How to get a MapTiler API key** - Create a MapTiler account: [MapTiler](https://www.maptiler.com/) + **Create Account**
+ Sign up for a free account at [MapTiler](https://www.maptiler.com/)
- Go to your dashboard and create an **API key**. + **Get API Key**
+ Go to the **Keys** section in your **MapTiler dashboard** and copy your **Default Key**.
- Copy the key and set it as `MAPTILER_API_KEY`. + **Update `.env`**
+ Paste the key into your `.env` file. + ```bash copy + # Replace with your API Key. + MAPTILER_API_KEY=__MAPTILER_API_KEY__ + ```
-```bash copy -# Replace with your API Key. -MAPTILER_API_KEY=__MAPTILER_API_KEY__ -``` +[MapTiler Documentation](https://docs.maptiler.com/) + --- ##### MQTT (Real-time Data) -> The Dashboard connects to MQTT to receive real-time device data. +Required -**Recommended values**
-**Development** -```bash copy -DASHBOARD_MQTT_PROTOCOL=ws -DASHBOARD_MQTT_PORT=8883 -DASHBOARD_MQTT_BROKER=emqx.localhost:8000 -``` +The Dashboard connects directly to the MQTT Broker via **WebSockets** to display real-time charts and device status updates. Without this, the dashboard will not update live. -**Production** -```bash copy -# Use wss (secure WebSocket) in production. -DASHBOARD_MQTT_PROTOCOL=wss -DASHBOARD_MQTT_PORT=443 -DASHBOARD_MQTT_BROKER=emqx.example.com -``` +**Configuration by Environment:** -- `DASHBOARD_MQTT_USERNAME` / `DASHBOARD_MQTT_PASSWORD` - Credentials used by the Dashboard to connect to the MQTT broker. -- `DASHBOARD_MQTT_PROTOCOL` - Protocol used for MQTT connections.
-Use ws (WebSocket) for browser-based clients. -- `DASHBOARD_MQTT_PORT` - Port exposed by the MQTT broker for WebSocket connections. -- `DASHBOARD_MQTT_BROKER` - Host and port of the MQTT broker accessible from the browser. + + + For Localhost: Use `ws` (Unencrypted WebSocket) for local development. + ```bash copy + # 1. Connection Details + DASHBOARD_MQTT_PROTOCOL=ws + DASHBOARD_MQTT_PORT=8083 + DASHBOARD_MQTT_BROKER=localhost + + # 2. Public Auth (Read-Only User recommended) + DASHBOARD_MQTT_USERNAME=dashboard_user + DASHBOARD_MQTT_PASSWORD=your_password + ``` + > (Note: Ensure Port `8083` is exposed in your EMQX Docker container). + + + + For Production: You **must** use `wss` (Secure WebSocket). Browsers will block insecure `ws` connections if your site is loaded over `https`. + ```bash copy + # 1. Connection Details + DASHBOARD_MQTT_PROTOCOL=wss + DASHBOARD_MQTT_PORT=443 + DASHBOARD_MQTT_BROKER=mqtt.yourdomain.com + + # 2. Public Auth (Read-Only User recommended) + DASHBOARD_MQTT_USERNAME=dashboard_user + DASHBOARD_MQTT_PASSWORD=your_password + ``` + + + +**Variable Details:** + - `DASHBOARD_MQTT_PROTOCOL`: `ws` for local, `wss` for production (SSL). + - `DASHBOARD_MQTT_BROKER`: The domain pointing to your MQTT Broker. + - `DASHBOARD_MQTT_PORT`: The WebSocket port (Default EMQX: `8083` for ws, `8084` for wss). + + + ⚠️ **Browser Security Rule:** If your Dashboard runs on **HTTPS** (Production), you cannot use ws (insecure).
+ You MUST use `wss`. Failing to do so will cause "Mixed Content" errors, and the connection will fail. +
+ +[MQTT over WebSocket](https://www.emqx.com/en/blog/connect-to-mqtt-broker-with-websocket) + +--- + +
+

+ 🎉 Configuration Fully Complete! +

+
+ You have successfully configured the Backend APIs, Admin Portal, and User Dashboard. Your SpaceDF environment is ready. +
+
+ 👉 Next Step: Proceed to the Starting the Services section to launch your platform. +
+
- - Use HTTPS and `wss` in production environments
- Restrict MQTT permissions to read-only topics for the Dashboard -
--- -##### References -- [NextAuth.js Documentation](https://next-auth.js.org/) -- [MapTiler Documentation](https://docs.maptiler.com/) -- [MQTT over WebSocket](https://www.emqx.com/en/blog/connect-to-mqtt-broker-with-websocket) ## Starting and Stopping -> This section explains how to start, check, and stop SpaceDF services using Docker Compose. -### Starting the services -From the directory that contains your `./entrypoint.sh` file, run: +This section explains how to manage the lifecycle of your SpaceDF instance using standard Docker commands. + + + ⚠️ **Prerequisite:** Ensure your `.env` file is fully configured and saved in the root directory before running these commands. + + +### Starting the Services +To launch SpaceDF, you will use the provided entrypoint script. This script handles the initialization and orchestrates the Docker containers for you. + +**Run the following commands in your terminal:** ```bash copy -# Start all services in the background +# 1. Grant execution permission (First time only) chmod +x entrypoint.sh + +# 2. Start the system ./entrypoint.sh ``` -Docker will start all SpaceDF services in detached mode. +**What happens next?** +- The script will trigger Docker to download necessary images and start all services in **detached mode** (background). +- You can close the terminal window without stopping the application. + + + 🔍 **Troubleshooting:** If you see a `Permission denied` error, ensure you have run the `chmod +x` command successfully. + ### Checking service status -After starting, you can check the status of all services: +After running the startup script, verify that all containers are functioning correctly. + +**1. Check Status Command** Run the following command to list all active services: + ```bash copy docker compose ps ``` +**2. Expected Output** Give the system about **30-60 seconds** to initialize. You should see a list of services where the status column shows `Up (healthy)`. + Within a minute, most services should show a status similar to: -```scss copy -Up (healthy) +```text +NAME IMAGE STATUS PORTS +spacedf-backend spacedf/backend Up (healthy) 0.0.0.0:8000->8000/tcp +spacedf-dashboard spacedf/dashboard Up (healthy) 0.0.0.0:3000->3000/tcp +emqx emqx/emqx Up (healthy) 1883/tcp, 8083/tcp... +postgres postgres:15 Up (healthy) 5432/tcp ``` + + ℹ️ **Note on "starting" status:** If you see `Up (starting)`, it means the service is still initializing (e.g., waiting for the database). Wait a moment and run the command again. + + + + 🔍 **Troubleshooting:** If any service shows `Exited (1) or Unhealthy`, please check the logs using: `docker compose logs -f <service_name>` + + ### Troubleshooting startup issues -If a service shows a status like `created` or does not become Up, check its logs to see what went wrong. +If a service shows a status like `Exited`, `Restarting`, or remains `Created` for too long, it means the container failed to start correctly. + +**1. View Service Logs** To identify the specific error, inspect the logs of the problematic service. -For example, to view logs for a specific service: ```bash copy -docker compose logs analytics +# Syntax: docker compose logs -f +docker compose logs -f backend ``` -Review the logs for errors such as missing environment variables or connection issues. +- `-f`: Follows the log output in real-time (press `Ctrl+C` to exit). +- Replace `backend` with the name of the failing service (e.g., `dashboard`, `emqx`, `postgres`) + +**2. Common Error Patterns** When reviewing the logs, look for these common issues: + +| Error Type | Log Message Example | Solution | +|----------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------------------| +| Missing Config | `KeyError: 'DATABASE_URL' or Config validation failed` | Check your `.env` file. A required variable is missing or empty. | +| Connection | `Connection refused or PGSQL connection failed` | The service cannot reach a dependency (like the Database). Wait a moment, or check if it runs. | +| Permission | `EACCES: permission denied` | File permission issue (often happens with scripts). Run `chmod +x` on entry scripts. | + + + ⭐ **Pro Tip:** If you see `Connection refused` immediately after starting, wait 10-20 seconds. Sometimes services start faster than the Database, causing a temporary error before they automatically retry and succeed. + ### Stopping the services -To stop all running SpaceDF services, run: + +To gracefully shut down the SpaceDF platform and release system resources, run the following command: + ```bash copy docker compose down ``` -This stops and removes the containers but keeps your data volumes intact. - - **Notes** - - Starting and stopping services may take a short time - - Always check service status after starting - - Stop services before making configuration changes - +**Data Persistence:** Rest assured, this command **only removes the containers**. Your database, user accounts, and device logs are stored in persistent volumes and **will be preserved**. + + + - **When to stop:** It is recommended to stop the services before making major changes to the `.env` file or `docker-compose.yml`. + - **Restarting:** To apply new configurations or updates, simply run `./entrypoint.sh` (or `docker compose up -d`) again after stopping. + + + + ⛔ **Caution:** Do not use the `-v` flag (e.g., `docker compose down -v`) unless you intend to **permanently delete** all your data and start from scratch. + + ## Accessing SpaceDF Services -> After all services are running, you can access SpaceDF through the web interfaces and backend APIs using the URLs below. + +Once all containers are running successfully, you can access the various components of the SpaceDF platform using the URLs below.
@@ -1002,31 +1558,71 @@ This stops and removes the containers but keeps your data volumes intact.
- - **Notes** - - Make sure all services show **Up (healthy)** before accessing them - - Use HTTPS in production environments - - Ensure firewall and reverse proxy settings allow access to the required ports - + + - **Apps (Dashboard/Admin):** Log in using the `OWNER_EMAIL` and `OWNER_PASSWORD` you defined in the `.env` file. + - **EMQX Console:** Log in using `admin` / `public` (Default) or the credentials defined in `EMQX_PASSWORD`. + + +**Production URLs**
+If you have deployed to a live server, replace `localhost` with your configured domains: + +- Dashboard: `https://dashboard.yourdomain.com` +- Admin Portal: `https://admin.yourdomain.com` +- API: `https://api.yourdomain.com` ## Updating -SpaceDF publishes stable updates for the Docker Compose setup on a regular basis. -To update your self-hosted deployment, pull the latest changes from the repository and restart the services. +SpaceDF frequently releases stable updates to introduce new features, security patches, and performance improvements. To keep your self-hosted instance running smoothly, we recommend keeping it up to date. -> Updating services requires restarting containers and may cause temporary downtime. + + ⚠️ **Downtime Notice:** The update process requires restarting Docker containers. Your services will be temporarily unavailable (usually for a few seconds to a minute) during the restart phase. Plan accordingly. + + +**Standard Update Procedure** + +Run the following commands in your terminal to upgrade your deployment. -### General update process - Pull the latest changes from the SpaceDF repository. + **Fetch Latest Configuration**
+ Update your local repository to get the latest `docker-compose.yml` and script changes. + ```bash copy + git pull origin main + ```
- - Pull updated Docker images. + + + **Download Updated Images**
+ Pull the latest versions of the SpaceDF Docker images. + ```bash copy + docker compose pull + ```
- - Restart the services. + + + **Apply & Restart**
+ Recreate the containers with the new images. + ```bash copy + docker compose up -d + ``` + > (Note: Docker will only restart containers that have updates).
-In most cases, this is enough to apply the latest updates. + + ⭐ **Cleanup (Optional):** After a major update, you can remove old, unused images to free up disk space: `docker image prune -f` + ### Updating individual services (advanced) You can run a specific version of a service by changing its Docker image tag in the `docker-compose.yml` file. @@ -1036,87 +1632,129 @@ You can run a specific version of a service by changing its Docker image tag in This approach is recommended only if you know exactly what you are doing. -#### Update or rollback a single service +### Advanced: Pinning & Rolling Back Versions + +In specific scenarios—such as rolling back a buggy update or testing a beta feature—you may need to force a specific version of a service instead of using `latest`. + + + ⛔ **Compatibility Risk:** SpaceDF services are designed to work together as a suite. Running mixed versions (e.g., a simplified Dashboard with an advanced Backend) may lead to **API errors**, **database conflicts**, or **data corruption**. + + **Only proceed if you understand the dependencies**. + + +**Procedure: Update a Single Service** + +Follow these steps to update (or downgrade) a specific component, for example, the **Dashboard**. -For example, if you want to update or roll back the Dashboard service: - Check available images and tags on the [SpaceDF container registry](https://github.com/orgs/Space-DF/packages). + **Find the Version Tag**
+ Visit the [SpaceDF Container Registry](https://github.com/orgs/Space-DF/packages) to find the exact tag you need (e.g., `v2026.01.21`).
- Choose a version tag (for example: `v2026.01.21`). - - - Update the image field in `docker-compose.yml` - ```yml copy - image: ghcr.io/space-df/spacedf-web-app:v2026.01.21 + **Edit Configuration**
+ Open `docker-compose.yml` and locate the service (e.g., `dashboard`). Replace the image tag with your chosen version. + ```yml copy + services: + dashboard: + # Change the tag after the colon + image: ghcr.io/space-df/spacedf-web-app:v2026.01.21 ```
- - Apply the changes: + + **Apply Changes (Smart Restart)**
+ Run the following commands to pull and restart **only** the target service, leaving the rest of the system running. ```bash copy - docker compose pull - docker compose down - docker compose up -d - ``` + # 1. Pull the specific image + docker compose pull dashboard + + # 2. Recreate only this container + docker compose up -d dashboard + ```
-The service will restart using the specified version. + + ❓ **Why not use `docker compose down`** Using `down` stops the entire platform. By targeting `up -d dashboard`, you only restart the Dashboard, ensuring that the Backend and MQTT Broker continue to process device data without interruption. + -### Notes on downtime -- Services must be restarted to apply updates -- Active users may experience brief downtime -- Consider performing updates during low-traffic periods +### Maintenance Strategy & Resources -### Staying up to date -- To track changes, fixes, and new features, refer to: - - SpaceDF release notes -- The self-hosting changelog in the repository +Managing updates effectively is key to maintaining a stable IoT platform. -## Uninstalling + + - **Restart Required:** Applying updates always requires restarting the Docker containers. + - **Service Interruption:** Active users will experience a brief disconnection (typically 10-60 seconds) while the containers re-initialize. + - **Best Time to Update:** We strongly recommend scheduling maintenance during **low-traffic periods** (e.g., late night or weekends) to minimize the impact on your operations. + - - **Warning**
- The steps below will permanently delete all SpaceDF data, including databases and storage volumes.
- Make sure you have backups before continuing. -
+**Staying Up to Date:** To track the latest features, security patches, and bug fixes, please consult our official channels: +- [SpaceDF Release Notes](https://docs.spacedf.com/blog): High-level overview of new features and major changes. +- [GitHub Changelog](https://github.com/Space-DF): Detailed technical log of all changes in the self-hosted repository. -### Stop and remove services -From the directory that contains your `docker-compose.yml` file, run: -```bash copy -# Stop all services and remove containers and volumes -docker compose down -v -``` +## Uninstalling -This command stops all SpaceDF services and removes all associated Docker volumes. +This section guides you through completely removing SpaceDF from your server. -### (Optional) Remove remaining data manually -In some cases, you may want to ensure all data directories are fully removed. + + - User accounts and passwords. + - All device data and history. + - System configurations. + - Uploaded files. + -**Remove database data** -```bash copy + +### Wipe Containers & Volumes + Run the following command in your project directory to stop services and destroy their internal data volumes. + ```bash copy + # The '-v' flag ensures volumes are deleted + docker compose down -v + ``` +### Clean Host Directories (Optional) +Docker might leave some mounted files on your host machine (usually inside the `volumes/ folder`). To ensure a 100% clean state, remove them manually. + +```bash copy +# Remove Database files rm -rf volumes/db/data -``` -**Remove storage data** -```bash copy +# Remove File Storage (Uploads/Images) rm -rf volumes/storage ``` + -**What gets removed** -- All SpaceDF services -- All databases and stored data -- All uploaded files and assets +### Verification -After these steps, SpaceDF is completely removed from your system. +At this point, SpaceDF is completely removed. +- Containers: Stopped and deleted. +- Networks: Removed. +- Data: Wiped. - - **Notes** - - These actions **cannot be undone** - - Use this only if you want a clean uninstall - - For reinstallation, follow the setup guide from the beginning - - -## Demo + + ℹ️ **Reinstallation:** If you want to start fresh, simply run the setup guide from the beginning. The system will treat it as a brand new installation. + ## Need Help? diff --git a/src/content/getting-started/self-hosting/docker/quick-start.mdx b/src/content/getting-started/self-hosting/docker/quick-start.mdx index d6e5be0..40eece6 100644 --- a/src/content/getting-started/self-hosting/docker/quick-start.mdx +++ b/src/content/getting-started/self-hosting/docker/quick-start.mdx @@ -8,7 +8,7 @@ import { CardInfo } from "@/app/_components/CardInfo.jsx" # Quick Start -> This guide helps you get **SpaceDF running quickly** on your own infrastructure using **Docker Compose** and **default settings**. +This guide helps you get **SpaceDF running quickly** on your own infrastructure using **Docker Compose** and **default settings**.
    @@ -117,7 +117,8 @@ After startup, SpaceDF services are available on the following default ports: Make sure these ports are open on your server and not used by other services. ### Default account (Quick Start only) -> Quick Start creates default accounts for initial access + +Quick Start creates default accounts for initial access These accounts are for **local testing only**. diff --git a/src/content/getting-started/self-hosting/index.mdx b/src/content/getting-started/self-hosting/index.mdx index 76fb78e..2ce6566 100644 --- a/src/content/getting-started/self-hosting/index.mdx +++ b/src/content/getting-started/self-hosting/index.mdx @@ -8,7 +8,7 @@ import { RichCardOptions, RichCardOption } from "@/app/_components/RichCard.jsx" # Self-Hosting -> Install and run your own SpaceDF platform on your computer, server, or cloud infrastructure. +Install and run your own SpaceDF platform on your computer, server, or cloud infrastructure. --- From 454da83c3dd99e615b94906719ded82532ee4334 Mon Sep 17 00:00:00 2001 From: Quynh-Nguyen Date: Thu, 29 Jan 2026 09:02:40 +0000 Subject: [PATCH 3/3] Trigger Build