---
name: sonamu-config
description: Sonamu project configuration. .env environment variables, sonamu.config.ts settings. Use when configuring a new Sonamu project.
---

# Sonamu Project Configuration

A guide for setting up `.env` and `sonamu.config.ts` after project creation.

## .env File

Location: `packages/api/.env`

### Default Environment Variables (generated by create-sonamu)

```env
# Database Configuration
DB_HOST=0.0.0.0
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=1234
CONTAINER_NAME=myproject-container
DATABASE_NAME=myproject
PROJECT_NAME=myproject
```

### Additional Environment Variables (as needed)

```env
# Session (must be changed in production)
SESSION_SECRET=your-secret-key-change-in-production
SESSION_SALT=random-16-char-salt

# AWS S3 (for file uploads)
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
S3_REGION=ap-northeast-2
S3_BUCKET=your-bucket-name

# Slack notifications (for migration confirmation)
SLACK_BOT_TOKEN=xoxb-your-token
SLACK_CHANNEL_ID=C0123456789
```

### Environment Variable Reference

| Variable                | Required | Description                                               |
| ----------------------- | -------- | --------------------------------------------------------- |
| `DB_HOST`               | ✓        | DB host (Docker: `0.0.0.0`, external DB: the relevant IP) |
| `DB_PORT`               | ✓        | DB port (default: `5432`)                                 |
| `DB_USER`               | ✓        | DB username (default: `postgres`)                         |
| `DB_PASSWORD`           | ✓        | DB password                                               |
| `DATABASE_NAME`         | ✓        | Database name                                             |
| `PROJECT_NAME`          | ✓        | Project name (used in Docker, config)                     |
| `CONTAINER_NAME`        |          | Docker container name                                     |
| `SESSION_SECRET`        |          | Session encryption key (required in production)           |
| `SESSION_SALT`          |          | Session salt (16 characters)                              |
| `AWS_ACCESS_KEY_ID`     |          | Required when using S3                                    |
| `AWS_SECRET_ACCESS_KEY` |          | Required when using S3                                    |
| `S3_REGION`             |          | S3 region (default: `ap-northeast-2`)                     |
| `S3_BUCKET`             |          | S3 bucket name                                            |

---

## sonamu.config.ts

Location: `packages/api/src/sonamu.config.ts`

### Full Structure

```typescript
import path from "node:path";
import { CachePresets, defineConfig } from "sonamu";
import { drivers as cacheDrivers, store } from "sonamu/cache";
import { drivers } from "sonamu/storage";

const host = "localhost";
const port = 34900;

export default defineConfig({
  projectName: process.env.PROJECT_NAME ?? "MyProject",
  api: {
    /* API settings */
  },
  i18n: {
    /* i18n settings */
  },
  sync: {
    /* sync settings */
  },
  database: {
    /* DB settings */
  },
  logging: {
    /* logging settings (disable with false) */
  },
  test: {
    /* test settings */
  },
  server: {
    /* server settings */
  },
  slackConfirm: {
    /* Production migration Slack approval */
  },
});
```

### Key Section Settings

#### projectName

```typescript
projectName: process.env.PROJECT_NAME ?? "MyProject",
```

#### api

```typescript
api: {
  dir: "api",              // API directory name
  timezone: "Asia/Seoul",  // timezone
  route: {
    prefix: "/api",        // API route prefix
  },
},
```

#### i18n

```typescript
i18n: {
  defaultLocale: "ko",
  supportedLocales: ["ko", "en"],
},
```

See `i18n.md` for details.

#### sync

```typescript
sync: {
  targets: ["web"],  // target packages for type sync
},
```

#### database

```typescript
database: {
  database: "pg",  // PostgreSQL
  name: process.env.DATABASE_NAME ?? "database_name",
  defaultOptions: {
    connection: {
      host: process.env.DB_HOST || "0.0.0.0",
      port: Number(process.env.DB_PORT) || 5432,
      user: process.env.DB_USER || "postgres",
      password: process.env.DB_PASSWORD,
    },
  },
},
```

See `database.md` for details.

#### test

```typescript
test: {
  parallel: true,   // enable parallel testing
  maxWorkers: 4,    // number of workers
  devRunner: {      // Vitest resident instance inside Dev server
    enabled: true,
    watch: true,
  },
},
```

See `testing.md`, `testing-devrunner.md` for details.

#### server

```typescript
server: {
  listen: { port: 34900, host: "localhost" },
  plugins: { /* plugin settings */ },
  auth: true,
  apiConfig: { /* API settings */ },
  storage: { /* storage settings */ },
  cache: { /* cache settings */ },
  lifecycle: { /* lifecycle hooks */ },
},
```

---

## server.auth Details (better-auth Authentication)

Sonamu provides an authentication system using **better-auth**.

### 1. Auto-generate Entities

```bash
pnpm sonamu auth generate
```

Generated entities:

- **User** - user (id, name, email, email_verified, image)
- **Session** - session (token, expires_at, user_id)
- **Account** - account (provider_id, access_token, etc.)
- **Verification** - email verification

### 2. server.auth Configuration

```typescript
server: {
  // Basic configuration (emailAndPassword enabled)
  auth: {
    emailAndPassword: { enabled: true },
  },

  // Add social login
  // auth: {
  //   emailAndPassword: { enabled: true },
  //   socialProviders: {
  //     google: {
  //       clientId: process.env.GOOGLE_CLIENT_ID!,
  //       clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  //     },
  //   },
  // },
}
```

### 3. Authentication API Endpoints

Automatically registered under the `/api/auth/*` path:

| Endpoint                  | Method | Description |
| ------------------------- | ------ | ----------- |
| `/api/auth/sign-up/email` | POST   | Sign up     |
| `/api/auth/sign-in/email` | POST   | Sign in     |
| `/api/auth/sign-out`      | POST   | Sign out    |
| `/api/auth/get-session`   | GET    | Get session |

### 4. Accessing user/session from Context

```typescript
@api({ httpMethod: "GET", guards: ["user"] })
async me(): Promise<UserSubsetA | null> {
  const { user, session } = Sonamu.getContext();
  if (!user) return null;
  return this.findById("A", user.id);
}
```

### 5. Field Mapping (camelCase → snake_case)

better-auth uses camelCase, Sonamu uses snake_case. Automatic mapping is applied:

| better-auth     | Sonamu           |
| --------------- | ---------------- |
| `emailVerified` | `email_verified` |
| `createdAt`     | `created_at`     |
| `userId`        | `user_id`        |
| `expiresAt`     | `expires_at`     |

---

## Guards System (Access Control)

The Sonamu permission system consists of 2 components:

1. **GuardKeys** - permission key definitions
2. **guardHandler** - permission check logic

### 1. Extending GuardKeys (Custom Permissions)

**Source code:** `modules/sonamu/src/api/decorators.ts` (GuardKeys interface)

Provided by default: `query`, `admin`, `user`

To add custom permissions, extend in `src/typings/sonamu.d.ts`:

**File location:** `src/typings/sonamu.d.ts`

```typescript
import {} from "sonamu";

declare module "sonamu" {
  export interface GuardKeys {
    query: true;
    admin: true;
    user: true;
    manager: true; // added
    superadmin: true; // added
  }
}
```

### 2. Using guards in the @api Decorator

```typescript
// user.model.ts
import { api } from "sonamu";

class UserModelClass extends BaseModelClass {
  @api({ httpMethod: "GET", guards: ["user"] })
  async me(): Promise<UserSubsetA | null> {
    // only logged-in users can access
  }

  @api({ httpMethod: "DELETE", guards: ["admin"] })
  async del(ids: number[]): Promise<number> {
    // only admins can access
  }

  @api({ httpMethod: "GET", guards: ["admin", "manager"] })
  async adminList(): Promise<UserSubsetA[]> {
    // admin or manager permission
  }
}
```

### 3. Implementing guardHandler

```typescript
import { Sonamu } from "sonamu";

// sonamu.config.ts
apiConfig: {
  guardHandler: (guard, request, api) => {
    // Access user from better-auth Context
    const { user } = Sonamu.getContext();

    switch (guard) {
      case "user":
        // login required
        if (!user) {
          throw new Error("Login is required");
        }
        break;

      case "admin":
        // admin permission (requires adding role field to User entity)
        if (!user || (user as any).role !== "admin") {
          throw new Error("Only admins can access");
        }
        break;

      case "manager":
        // manager permission (custom Guard example)
        if (!user || !["admin", "manager"].includes((user as any).role)) {
          throw new Error("Manager permission is required");
        }
        break;

      case "query":
        // allow all users (including unauthenticated)
        break;
    }
  },
},
```

**NOTE:** better-auth's default User entity does not have a `role` field. If role-based authentication is needed, add a `role` field to the User entity or create a separate Role entity.

### Menu/Screen Access Control by Permission

UI access control by permission is handled **on the frontend**:

```typescript
// web/src/lib/auth.ts
export const menuPermissions = {
  dashboard: ["user", "admin", "manager"],
  userManagement: ["admin"],
  settings: ["admin", "manager"],
  reports: ["admin", "manager"],
};

export function canAccess(userRole: string, menu: keyof typeof menuPermissions) {
  return menuPermissions[menu].includes(userRole);
}
```

```tsx
// web/src/components/Sidebar.tsx
{
  canAccess(user.role, "userManagement") && (
    <MenuItem href="/admin/users">User Management</MenuItem>
  );
}
```

---

## server.plugins Details

### session (Session Management)

```typescript
session: {
  secret: process.env.SESSION_SECRET || "change-this-in-production",
  salt: process.env.SESSION_SALT || "mq9hDxBCDbsQDR6N",
  cookie: {
    domain: "localhost",  // change to actual domain in production
    path: "/",
    maxAge: 60 * 60 * 24 * 365 * 10,  // 10 years
  },
},
```

**Production checklist:**

- `SESSION_SECRET`: must be changed to a strong random string
- `SESSION_SALT`: change to a 16-character random string
- `cookie.domain`: change to the actual domain

### static (Static Files)

```typescript
static: {
  root: path.join(import.meta.dirname, "/../", "public"),
  prefix: "/api/public",
},
```

### multipart (File Upload)

```typescript
multipart: {
  limits: {
    fileSize: 1024 * 1024 * 30,  // 30MB
  },
},
```

---

## server.storage Details

### Local File System

```typescript
storage: {
  drivers: {
    fs: drivers.fs({
      location: path.join(import.meta.dirname, "/../public/uploaded"),
      visibility: "public",
      urlBuilder: {
        async generateURL(key) {
          return `/api/public/uploaded/${key}`;
        },
        async generateSignedURL(key) {
          return `/api/public/uploaded/${key}`;
        },
      },
    }),
  },
},
```

### AWS S3

```typescript
storage: {
  drivers: {
    s3: drivers.s3({
      credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "",
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "",
      },
      region: process.env.S3_REGION ?? "ap-northeast-2",
      bucket: process.env.S3_BUCKET ?? "my-bucket",
      visibility: "private",
    }),
  },
},
```

---

## server.cache Details

Sonamu uses **BentoCache**.

```typescript
import { drivers as cacheDrivers, store } from "sonamu/cache";

cache: {
  default: "main",
  stores: {
    main: store().useL1Layer(cacheDrivers.memory({ maxSize: "50mb" })),
  },
  ttl: "5m",
  prefix: "",
},
```

**Available drivers:**

- `memory` - in-memory cache (default)
- `file` - file-based cache
- `redis` - Redis cache
- `knex` - DB-based cache

For other drivers, refer to the [BentoCache documentation](https://bentocache.dev/).

---

## server.apiConfig Details

### contextProvider

Inject additional information into Context per request:

```typescript
contextProvider: (defaultContext, request) => {
  return {
    ...defaultContext,
    ip: request.ip,
    session: request.session,
    body: request.body,
    // custom fields can be added
  };
},
```

### guardHandler

Handle API guard processing:

```typescript
guardHandler: (guard, request, api) => {
  // access control based on guard value
  if (guard === "admin" && request.user?.role !== "admin") {
    throw new Error("Only admins can access");
  }
},
```

### cacheControlHandler

Set HTTP cache headers:

```typescript
cacheControlHandler: (req) => {
  switch (req.type) {
    case "assets":
      if (req.path.match(/-[a-f0-9]+\./)) {
        return CachePresets.immutable;  // files with hash
      }
      return CachePresets.longLived;

    case "api":
      if (req.method === "GET") {
        return CachePresets.shortLived;
      }
      return CachePresets.noCache;

    case "ssr":
      return CachePresets.ssr;

    case "csr":
      return CachePresets.shortLived;
  }
},
```

---

## server.lifecycle Details

```typescript
lifecycle: {
  onStart: () => {
    console.log(`🌲 Server listening on http://${host}:${port}`);
  },
  onShutdown: () => {
    console.log("graceful shutdown");
    // close DB connections, clean up resources, etc.
  },
  onError: (error, request, reply) => {
    console.error(error);
    reply.status(500).send({
      name: error.name,
      message: error.message,
    });
  },
},
```

---

## Sonamu Local Development Environment Setup

**When is this needed:**

- When modifying the Sonamu framework source code during development
- When linking a local Sonamu repository to a project for development

**Problem:**

When linking Sonamu with pnpm link, type errors occur at build time:

```
error TS2345: Argument of type 'ZodNumber' is not assignable to parameter...
  Type '2' is not assignable to type '3'.
```

**Cause:**

- The linked Sonamu and the project each maintain their own `node_modules`
- TypeScript type mismatches occur due to different versions of shared dependencies (e.g. zod)
- TypeScript simultaneously references two different type definitions, causing errors

**Solution:**

### 1. Add override to pnpm-workspace.yaml

In the project root's `pnpm-workspace.yaml`:

```yaml
overrides:
  sonamu: link:../../sonamu/modules/sonamu
```

### 2. Specify published version in packages/api/package.json

```json
{
  "dependencies": {
    "sonamu": "^0.7.45" // specify the latest published version
  }
}
```

### 3. Run install

```bash
pnpm install
```

### 4. Verify build

```bash
cd packages/api
pnpm build
```

**How it works:**

- **TypeScript type check**: references type definitions from the npm registry based on the published version in `package.json`
- **Actual runtime**: `pnpm overrides` local link takes priority and runs local source code
- Separates type checking and runtime to resolve version mismatch issues

**Notes:**

- Changes to Sonamu source code are immediately reflected in the project
- Restarting the project is required after building Sonamu
- For general project development, using the npm version is recommended

---

## Environment-Specific Configuration

### Development Environment

```env
# packages/api/.env
DB_HOST=0.0.0.0
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=1234
DATABASE_NAME=myproject
PROJECT_NAME=myproject
```

### Production Environment

```env
# packages/api/.env.production
DB_HOST=your-rds-endpoint.amazonaws.com
DB_PORT=5432
DB_USER=produser
DB_PASSWORD=strong-password-here
DATABASE_NAME=myproject_prod
PROJECT_NAME=myproject

SESSION_SECRET=very-long-random-string-at-least-32-chars
SESSION_SALT=random16charstr!

AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
S3_REGION=ap-northeast-2
S3_BUCKET=myproject-prod-bucket
```

---

## server Additional Options

### baseUrl

```typescript
server: {
  baseUrl: "https://api.example.com",  // external access URL (default: host:port)
}
```

### fastify

Pass Fastify server options directly (excluding `logger`).

### Full Plugin List

| Plugin      | Type                                 | Description                              |
| ----------- | ------------------------------------ | ---------------------------------------- |
| `compress`  | `boolean \| FastifyCompressOptions`  | Response compression (@fastify/compress) |
| `cors`      | `boolean \| FastifyCorsOptions`      | CORS configuration                       |
| `formbody`  | `boolean \| FastifyFormbodyOptions`  | x-www-form-urlencoded parsing            |
| `multipart` | `boolean \| FastifyMultipartOptions` | File upload                              |
| `qs`        | `boolean \| QsPluginOptions`         | Query string parsing                     |
| `session`   | session config                       | Session management                       |
| `sse`       | `boolean \| SsePluginOptions`        | Server-Sent Events                       |
| `static`    | `boolean \| FastifyStaticOptions`    | Static file serving                      |
| `custom`    | `(server: FastifyInstance) => void`  | Custom plugin registration function      |

## logging

Define logging configuration. Set to `false` to completely disable logging.

```typescript
logging: false,  // disable logging
// or
logging: {
  sinks: { /* define log output targets */ },
  filters: { /* define filters */ },
},
```

## slackConfirm

Activates a Slack-based approval process for production DB migrations.

```typescript
slackConfirm: {
  targets: ["production"],       // list of DB keys requiring approval
  botToken: process.env.SLACK_BOT_TOKEN ?? "",  // Slack Bot Token (xoxb-...)
  channelId: process.env.SLACK_CHANNEL_ID ?? "", // Slack Channel ID (C...)
},
```

---

## Post-Configuration Checklist

1. Confirm `.env` file is created
2. Start Docker: `pnpm docker:up`
3. Verify build: `pnpm build`
4. Start server: `pnpm dev`
5. Access Sonamu UI: http://localhost:34900/sonamu-ui

Before production deployment:

- [ ] Change `SESSION_SECRET`
- [ ] Change `SESSION_SALT`
- [ ] Change `cookie.domain` to the actual domain
- [ ] Configure S3 (if needed)
- [ ] Add error handling logic
