Skip to main content

Overview

Kitsu follows a modern Vue.js architecture with clear separation of concerns. The application uses Vuex for state management, Vue Router for navigation, and a modular API client for backend communication.

Application Structure

Entry Point

The application bootstraps in src/main.js:
src/main.js
import { createApp } from 'vue'
import App from '@/App.vue'
import i18n from '@/lib/i18n'
import router from '@/router'
import store from '@/store'

const app = createApp(App)

app.use(i18n)
app.use(router)
app.use(store)

// Sync router with Vuex store
sync(store, router)

// Inject socket.io into store
store.$socket = app.config.globalProperties.$socket

app.mount('#app')
Key initializations:
  1. Create Vue app instance
  2. Install plugins (i18n, router, store, websocket)
  3. Sync router state with Vuex
  4. Mount to DOM

Router Architecture

Route Configuration

Routes are defined in src/router/routes.js and follow a hierarchical structure:
const routes = [
  {
    path: '/',
    component: MainLayout,
    children: [
      { path: 'productions', component: Productions },
      { path: 'assets', component: Assets },
      { path: 'shots', component: Shots }
    ]
  }
]

Router Setup

The router (src/router/index.js) uses:
  • HTML5 History Mode - Clean URLs without hash
  • Scroll Behavior - Restores scroll position on navigation
  • Navigation Guards - Authentication checks
src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  scrollBehavior: loadSavedScrollPosition,
  routes
})

Vuex Store Architecture

Store Structure

The Vuex store (src/store/index.js) uses a modular architecture:
src/store/index.js
import { createStore } from 'vuex'
import * as getters from '@/store/getters'

// Import modules
import assets from '@/store/modules/assets'
import shots from '@/store/modules/shots'
import tasks from '@/store/modules/tasks'
import people from '@/store/modules/people'
// ... more modules

export default createStore({
  getters,
  modules: {
    assets,
    shots,
    tasks,
    people
    // ...
  },
  strict: false
})

Store Modules

Each module follows a consistent pattern:
const state = {
  taskMap: new Map(),
  taskComments: {},
  selectedTasks: new Map(),
  isLoading: false
}

Key Store Modules

assets

Manages asset entities, types, and asset-related operations

shots

Handles shot entities, sequences, and episodes

tasks

Task management, comments, and previews

people

Team members, departments, and assignments

productions

Production/project data and settings

files

File management and output files

user

Current user state and preferences

login

Authentication and session management

API Client Architecture

Base Client

The API client (src/store/api/client.js) wraps Superagent:
src/store/api/client.js
import superagent from 'superagent'

const client = {
  // Promise-based GET
  pget(path) {
    return superagent.get(path).then(res => res?.body)
  },

  // Promise-based POST
  ppost(path, data) {
    return superagent
      .post(path)
      .send(data)
      .then(res => res?.body)
      .catch(err => {
        if (res?.statusCode === 401) {
          errors.backToLogin()
        }
        throw err
      })
  },

  // File upload with progress
  ppostFile(path, formData) {
    const request = superagent.post(path).send(formData)
    return {
      request,
      promise: new Promise((resolve, reject) => {
        request.end((err, res) => {
          if (err) reject(err)
          else resolve(res?.body)
        })
      })
    }
  }
}

API Modules

Each API module (src/store/api/*.js) provides domain-specific methods:
src/store/api/tasks.js
import client from '@/store/api/client'

export default {
  getTask(taskId) {
    return client.pget(`/api/data/tasks/${taskId}/full`)
  },

  updateTask(taskId, data) {
    return client.pput(`/api/data/tasks/${taskId}`, data)
  },

  commentTask(data) {
    return client.ppost(
      `/api/actions/tasks/${data.taskId}/comment`,
      data
    )
  }
}

Component Architecture

Component Organization

Components are organized by function:
components/
├── pages/       # Top-level page components
├── modals/      # Modal dialog components
├── lists/       # List and table components
├── widgets/     # Reusable widget components
├── cells/       # Table cell components
├── layouts/     # Layout components
├── tops/        # Top bar components
├── sides/       # Sidebar components
└── previews/    # Media preview components

Component Patterns

<template>
  <div class="page">
    <top-bar :title="title" />
    <entity-list :entities="assets" />
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'

export default {
  computed: {
    ...mapGetters(['assets', 'currentProduction'])
  },
  
  methods: {
    ...mapActions(['loadAssets'])
  },
  
  mounted() {
    this.loadAssets()
  }
}
</script>

Mixins

Reusable component logic is shared through mixins in src/components/mixins/:
export const taskMixin = {
  methods: {
    formatTaskStatus(task) {
      // Shared logic
    }
  }
}

State Management Patterns

When to Use Vuex

  • State is shared across multiple components
  • State needs to persist across route changes
  • Complex state transformations are required
  • Real-time updates affect multiple views

Data Flow

Real-time Updates

Kitsu uses Socket.io for real-time collaboration:
// Socket is injected into store
store.$socket.on('task:update', (data) => {
  store.commit(UPDATE_TASK, data)
})

store.$socket.on('comment:new', (data) => {
  store.commit(NEW_COMMENT, data)
})

Internationalization

Kitsu uses Vue I18n for multi-language support:
src/lib/i18n.js
import { createI18n } from 'vue-i18n'
import en from '@/locales/en'
import de from '@/locales/de'

const i18n = createI18n({
  locale: 'en',
  fallbackLocale: 'en',
  messages: { en, de }
})
In components:
<template>
  <h1>{{ $t('main.title') }}</h1>
</template>

Best Practices

Keep components focused

Each component should have a single, clear responsibility

Prefer composition

Use smaller, composable components over large monolithic ones

Minimize Vuex usage

Use local state when possible for better performance

Use getters for derived state

Don’t store computed values in state

Next Steps

Contributing Guide

Learn how to contribute code to Kitsu

Build docs developers (and LLMs) love