# Suno App UI

The Suno dot com website.

## Overview

This is a NextJS app that uses some SSR, but most of the fun stuff is happening
in the user's browser.

The tech stack is ever-evolving, but big picture things are internal component
library built on a couple third-party things, Tailwind CSS for styling, TanStack
Query for fetching and caching data, React Context and some legacy MobX for
state management.

Some things that you may see here and there that we _don't_ want to use are CSS
modules and Chakra UI.

Our in-house
[dsp-engine](https://github.com/suno-ai/glockenspiel/tree/main/dsp-engine) is
used for real-time audio manipulation and playback (primarily in the context of
creation, but not exclusively forever).

For more details, you can look at what we tell [Claude](./CLAUDE.md) or go
digging around the codebase.

### Tech stack

Ever-evolving

- NextJS - SSR and routing
- MobX - mutable state management
- Tailwind CSS - utility-first CSS framework
- openapi-fetch - Typescript code generation for OpenAPI APIs
- Clerk - Social Authentication (is a SaaS)
- React-Query (WIP) - Some helper hooks around data fetching - we don't really
  use this much right now.
- [dsp-engine](https://github.com/suno-ai/glockenspiel/tree/main/dsp-engine) -
  Real-time audio manipulation and playback

We rarely use CSS modules (tooling included with NextJS) due to using Chakra
responsive style props heavily (Jun 2025: Older components tend to use Chakra
UI, newer components are using Tailwind CSS, Some components use both e.g.,
AuraSubscriptionCard.tsx uses both twMerge for Tailwind and Chakra components).

**If you are suddenly seeing errors that mention `dsp-engine`, run
`pnpm prepare-dsp` (see below for more info).**

## Quick start

0. Make sure you have `studio_api` set up
1. Make sure your AWS SSO credentials are set up
2. Install npm dependencies
   ```shell
   pnpm install
   ```
3. Start the dev server
   ```shell
   pnpm dev
   ```

**If you are seeing errors that mention `dsp-engine`, run `pnpm prepare-dsp`**
(see below for more info).

## Updating Package / CLAUDE.md

If packages are updated or new best practices emerge, make sure to update the
[CLAUDE.md](./CLAUDE.md) file so that the coding agent has the most up to date
context on the codebase. This can be done by commiting to memory in the claude
code cli, or update the file manually.

## Development Setup

The web app listens on port 3000 and is configured to connect to a local backend
server on port 8000. You need both running to have a full working Suno
experience locally!

The instruction for setup backend server can be found
[here](https://github.com/suno-ai/glockenspiel/blob/main/studio_api/README.md)

### Running the Web App Locally

#### 1. Node.js (nvm or fnm)

We are currently using Node v22 LTS.

A version manager makes managing different Node installations easy, which comes
in handy when jumping between codebases or testing Node upgrades.

Install [fnm](https://github.com/Schniz/fnm) and use Node LTS (v22).

```shell
fnm install lts-latest
fnm default lts-latest
```

#### 2. Studio API and Tailscale

Studio API is required to make most of the app work.

```shell
tailscale funnel 8000
```

Tailscale forwards the Modal response to the local frontend app is is required
for song generation to work.

```shell
uv run manage.py runserver # in studio_api
```

#### 3. Env vars

Copy `.env.copy` in this dir and create a `.env` file. Get the necessary
environment variables from a teammate.

#### 4. pnpm

Install [pnpm](https://pnpm.io/installation), the Node package manager we use
for this monorepo.

```shell
pnpm install
```

#### 5. dsp-engine

Some parts of the app (editv3, studio) require the dsp-engine WebAssembly
module. Builds are available on a public S3 bucket. To pull the appropriate
build, run:

```shell
pnpm prepare-dsp
```

This will dump the build files in `ui/app-ui/dsp-artifacts`.

You may need to update the build as `dsp-engine` changes, or when switching
between branches that aren't in sync. Just run `pnpm prepare-dsp` again to fetch
the build appropriate to your checkout.

#### 6. Start dev server

Build and start the dev server:

```shell
pnpm dev
```

## Running Tests

### Unit Tests

We use Vitest for unit tests, including function, snapshot, and interaction
tests. CI runs them on PRs and will flag failed builds.

Do not merge a PR that is breaking the tests!

```shell
pnpm test # run the tests once
pnpm test:watch # run the tests in watch mode, for developnment
pnpm test:coverage # run the tests and collect coverage information
```

It's good to have most code covered by tests to avoid regressions, so it's
highly encouraged to add tests on an ongoing basis while writing new code.

#### Function tests

Classic unit tests. These are best for things like utilities where you have some
input or options that are passed in and want to verify an expected output that
you explicitly specify.

#### Snapshot tests

Snapshots are an easy way to validate consistent output of somethnig between
test runs. These are mostly used for checking the serialized output of things
like React components.

```jsx
import { render } from '@testing-library/react';

describe('MyComponent', () => {
  it('renders as expected', () => {
    const { container } = render(<MyComponent />);
    expect(container).toMatchSnapshot();
  });
});
```

Unlike regular function tests, the expected output is not specified explicitly.
The first time such a test is run, it takes a "snapshot" and writes it to the
`__snapshots__` directory--this assumes that the initial output is correct!

If a test generates different output in the future, it fails. If this failure is
expected because the code changed, you can update the snapshots:

```shell
pnpm test:update-snapshots
```

##### Unexpected snapshot errors

You may be surprised by snapshot errors that happen after adding new states to a
component that has snapshots automatically enumerated.

Such a failure might look like this:

```shell
Error: Snapshot `Button > size/shape/variant > renders XSmall:Pill:DarkSecondary as expected 1` mismatched
```

Updating the snapshots as described above should fix the problem. Just sanity
check that the HTML in the diff looks correct and properly scoped to your new
state to avoid poisoning the snapshots!

#### Interaction tests

These tests can be some of the most annoying to write, but the most valuable to
have, since they validate things that are closer to user interaction, such as
click handling and multi-step workflows.

```jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('MyComponent', () => {
  it('handles a click event', async () => {
    const user = userEvent.setup();
    const onClickSpy = vi.fn();
    render(<MyComponent data-testid='my-component' onClick={onClickSpy} />);
    const button = screen.getByTestId('my-component');
    await user.click(button);
    expect(onClickSpy).toHaveBeenCalledTimes(1);
  });
});
```

You can test React hooks in a similar way.

```jsx
import { act, renderHook } from '@testing-library/react';

describe('useMyHook', () => {
  test('initial count is zero', () => {
    const { result } = rendeHookr(() => useMyhook());
    expect(result.current.count).toBe(0);
  });
  test('increment the count', () => {
    const { result } = rendeHookr(() => useMyhook());
    act(() => {
      result.test.incrementCount();
    });
    expect(result.current.count).toBe(1);
  });
});
```

#### Test Coverage

Coverage refers to how many lines of code are actually run during the test
suite. We're gradually increasing the thresholds to help maintain better test
coverage.

```shell
pnpm test:update-coverage # run the tests and update coverage thresholds
```

### E2E Tests

E2E Tests run via a Github Action on every successful deployment of the `main`
branch.

#### Initial Setup

To setup auth for your local E2E testing and test codegen, ask another engineer
for the following env vars and put them in `ui/app-ui/.env.local`:

```
VERCEL_AUTOMATION_BYPASS_SECRET
E2E_TEST_USER_EMAIL
E2E_TEST_USER_PASSWORD
```

Then run the following command in `ui/app-ui` to populate your local
`auth-state.json`:

```shell
pnpm test:e2e
```

(This is also the command for running the E2E tests locally)

#### Codegen for E2E Tests

To run the codegen script, run:

```
pnpm test:e2e-codegen
```

You will be loaded into `https://b.suno.fm` in an already-authed state, and you
are ready to hit "Record" and generate an E2E test by clicking through the app
as desired in the Chromium emulator.

Your generated test code will appear in the Playwright Inspector which you can
then copy into a new `*.spec.ts` file under the `e2e/` directory, or add to an
existing test (usually there are minimal changes you need to make after copying
the generated test code).

_Note:_ You should make sure to add teardown steps to your test, so we avoid,
for example, creating thousands of workspaces as the automated E2E tests run
over time.

#### Running Tests

- Run all tests: `pnpm test:e2e`
- Run a specific test file: `pnpm test:e2e e2e/workspace-creation.spec.ts`
- Run tests with UI: `pnpm test:e2e --ui`
- Run tests in headed mode: `pnpm test:e2e --headed`

#### Debugging Failed Tests

- Tests can be configured to generate screenshots and videos on failure in the
  `test-results/` directory
- Use `test.only()` to focus on a specific test during development
- Add `await page.pause()` to pause test execution at a specific point

#### Common Issues & Solutions

- **Authentication problems**: If tests fail with auth issues, try regenerating
  your `auth-state.json` file
- **Selector issues**: Use the Playwright Inspector to verify and update
  selectors
- **Timing issues**: Use `await page.waitForSelector()` or increase timeouts for
  elements that load dynamically

#### CI/CD Integration

E2E tests run automatically on successful deployments to `main`. Results are
available in the GitHub Actions tab.

## API Handler Generation

To regenerate the API Typescript client from your local Django server, run the
following. You will need the local Django server running on port 8000.

```shell
pnpm gen-types
```

### (Optional) Feature flags and experiments

Feature flags and experiments are managed with [Statsig](https://statsig.com/).

### Overview of flag, model type configuration

TK

## Storybook

Storybook allows you to create interactive demos that are useful for visualizing
components and developing UI in isolation without running the entire web app.

The Storybook build using the latest code from `main` is available at
[storybook.suno.run](https://storybook.suno.run)

### Running Storybook

```shell
pnpm storybook
```

This will compile Storybook in dev mode and open the interface at
[localhost:6006](http://localhost:6006)

### Adding Stories

Story files should usually be 1:1 with the component files they're testing. For
example, a component defined in `MyComponent.tsx` would have a corresponding
`MyComponent.stories.tsx` file in the same directroy.

Here's a general template to define a Story:

```tsx
import type { Meta, StoryObj } from '@storybook/react';
import MyComponent, { MyComponentProps } from './MyComponent';

type PagePropsAndCustomArgs = React.ComponentProps<MyComponentProps>;

export default meta;
type Story = StoryObj<PagePropsAndCustomArgs>;

const meta: Meta<PagePropsAndCustomArgs> = {
  title: 'components/ComponentName',
  component: ComponentName,
};

export const Default: Story = {
  // `render` is optional. You can use it if you need some additional setup to
  // display the story, such as a styled container
  render: (...args) => (
    <div>
      <ComponentName {...args} />
    </div>
  );
  args: {
    className: 'text-accent-brand', // optioanl default argument
  }
}
```

## Bundle Analyzer

NextJS has a bundle analyzer that's built on top of Webpack Bundle Analyzer.
This is a tool that helps you understand the size of your bundle / dependencies
so that you can optimize your bundle size.

```shell
pnpm analyze
```

More information on bundle analyzer can be found
[here](https://nextjs.org/docs/advanced-features/analyze-bundle-size)

## Debugging / profiling the Next.js (node) server

Next.js does the SSR on a node server. To debug, you can:

- run `pnpm build && pnpm start-node`
  - this will build a production version of the app and start the node server
    with `NODE_OPTIONS='--inspect'` enabled
- open `chrome://inspect` in your browser
- under `Remote Targets` you should see a target to your running node server
- click "Inspect"
- this will open the devtools for your node server
- you can now use the devtools to inspect the server, memory, etc
- refresh your browser to profile how the server reacts
  - if you need more traffic, you can use a load testing tool like k6

taken from
[this article](https://medium.com/john-lewis-software-engineering/we-had-a-leak-identifying-and-fixing-memory-leaks-in-next-js-622977876697)

## Testing local changes on your actual device

- Run your django server on 0.0.0.0:8000
  (`uv run manage.py runserver 0.0.0.0:8000`)
- Run next.js dev server and copy the `Network` ip from the server logs
  - eg `- Network:      http://192.168.11.11:3000`
- Update your .env.local to have a value like
  `NEXT_PUBLIC_API_BASE_ECS="http://192.168.11.11:8000"`
  - note the port #. this is the ip of your machine + the port of the django
    server
- Update ALLOWED_HOSTS in
  [settings.py](https://github.com/suno-ai/glockenspiel/blob/9b5d69600f6f50263531112f0c50e36bccab7164/studio_api/studio_api/settings.py#L154)
  to have your local IP without the port
  - eg. `192.168.11.11`
- Update CORS_ALLOWED_ORIGIN_REGEXES in
  [settings.py](https://github.com/suno-ai/glockenspiel/blob/9b5d69600f6f50263531112f0c50e36bccab7164/studio_api/studio_api/settings.py#L154)
  to have your local IP + Next.js port #
  - eg. `r"http://192.168.11.11:3000"`
- hit your next.js network IP (eg. `http://192.168.11.11:3000`) from your phone
  and you should see your local env loading now!

## Deploying the UI to Production

You can run `/deploy-ui` in #tech-ui to trigger FE deploys. This runs a github
workflow that does functionally the following (which can also be used to deploy
manually):

```shell
git checkout main
git pull origin main
git checkout ui-prod
git pull origin ui-prod
git reset --hard origin/main # (or the commit sha instead of origin/main)
git push -f origin ui-prod
git checkout main
```

## Attaching cursor rules

As of Jun 2025, there are cursor rules
([docs](https://docs.cursor.com/context/rules)) for app-ui can be found in in
`ui/app-ui/.cursor.temp`. However, since we don't know if what differnce they'll
make, they are not enabled by default until we can dogfood a bit more and eval.

If you want to try using them, you can:

- Copy the file from `.cursor.temp` -> `.cursor/rules`
- Open the command palette (cmd + shift + p) -> Run `Cursor: View User Rules`
- Under `Project` you should see `ui/app-ui/.cursor/rules/app-ui.mdc` being
  applied now
- Test and leave feedback in
  [the feedback form](https://docs.google.com/document/d/1yt4moF7Yhx66lQtKo-soVikMQhSxnKH_JTxtoqQ0pVI/edit?tab=t.xj0sh7pd2tqt)
