# Discourse Platform Documentation ## Introduction Discourse is a comprehensive, open-source community platform built with Ruby on Rails and Ember.js. It provides a modern forum experience with real-time chat, advanced moderation tools, and extensive customization through a plugin architecture. The platform is designed to handle large-scale communities with features like multi-site support, sophisticated permission systems, and full-text search capabilities. Discourse has been battle-tested for over a decade and powers thousands of communities worldwide. The codebase is structured as a monolithic Rails application with an Ember.js frontend, supporting 44+ official plugins, extensive API endpoints, and a robust service-oriented architecture. It leverages PostgreSQL for data persistence, Redis for caching and real-time features, and Sidekiq for background job processing. The platform emphasizes accessibility, mobile responsiveness, and performance optimization, making it suitable for communities ranging from small hobby groups to large enterprise deployments. ## Core APIs and Functions ### Posts API - Create and Manage Forum Posts RESTful endpoint for creating, reading, updating, and deleting posts within topics. ```ruby # File: app/controllers/posts_controller.rb class PostsController < ApplicationController def create manager = NewPostManager.new( current_user, raw: params[:raw], topic_id: params[:topic_id], reply_to_post_number: params[:reply_to_post_number], category: params[:category], archetype: params[:archetype] ) result = manager.perform if result.success? post = result.post PostAlerter.post_created(post) render_serialized(post, PostSerializer) else render json: { errors: result.errors }, status: :unprocessable_entity end end def update post = Post.find(params[:id]) guardian.ensure_can_edit!(post) revisor = PostRevisor.new(post, current_user) revisor.revise!(current_user, { raw: params[:post][:raw] }) render_serialized(post, PostSerializer) end end # API Usage Example # POST /posts.json curl -X POST "https://discourse.example.com/posts.json" \ -H "Api-Key: YOUR_API_KEY" \ -H "Api-Username: username" \ -H "Content-Type: application/json" \ -d '{ "topic_id": 123, "raw": "This is the post content in Markdown format.", "reply_to_post_number": 1 }' # Response (201 Created) { "id": 456, "username": "user123", "created_at": "2025-01-15T10:30:00.000Z", "cooked": "
This is the post content in Markdown format.
", "post_number": 2, "topic_id": 123, "reply_count": 0 } # PUT /posts/:id.json curl -X PUT "https://discourse.example.com/posts/456.json" \ -H "Api-Key: YOUR_API_KEY" \ -H "Api-Username: username" \ -H "Content-Type: application/json" \ -d '{ "post": { "raw": "Updated post content with corrections." } }' ``` ### Topics API - Manage Discussion Topics Create and manage forum topics with metadata, category assignment, and visibility controls. ```ruby # File: app/controllers/topics_controller.rb class TopicsController < ApplicationController def show topic = Topic.find_by(id: params[:id]) guardian.ensure_can_see!(topic) opts = { page: params[:page]&.to_i || 1, post_number: params[:post_number], username_filters: params[:username_filters] } topic_view = TopicView.new(topic.id, current_user, opts) render_json_dump( TopicViewSerializer.new(topic_view, scope: guardian, root: false) ) end def create topic_params = { title: params[:title], raw: params[:raw], category: params[:category_id], tags: params[:tags], archetype: params[:archetype] || "regular" } topic_creator = TopicCreator.new(current_user, guardian, topic_params) topic = topic_creator.create render_serialized(topic, TopicSerializer) end def status topic = Topic.find(params[:topic_id]) guardian.ensure_can_moderate!(topic) status = params[:status] # "closed", "visible", "pinned", "archived" enabled = params[:enabled] == "true" TopicStatusUpdater.new(topic, current_user).update!(status, enabled) render json: success_json end end # API Usage Example # GET /t/:id.json curl "https://discourse.example.com/t/123.json" \ -H "Api-Key: YOUR_API_KEY" \ -H "Api-Username: username" # Response { "post_stream": { "posts": [ { "id": 456, "username": "user123", "cooked": "First post content
", "post_number": 1, "created_at": "2025-01-15T10:00:00.000Z" } ] }, "id": 123, "title": "How to use Discourse API", "category_id": 5, "views": 142, "like_count": 12, "posts_count": 8, "visible": true, "closed": false, "archived": false } # POST /posts.json (creates topic + first post) curl -X POST "https://discourse.example.com/posts.json" \ -H "Api-Key: YOUR_API_KEY" \ -H "Api-Username: username" \ -H "Content-Type: application/json" \ -d '{ "title": "New Topic Title", "raw": "This is the first post content.", "category": 5, "tags": ["api", "tutorial"] }' # PUT /t/:id/status.json curl -X PUT "https://discourse.example.com/t/123/status.json" \ -H "Api-Key: YOUR_API_KEY" \ -H "Api-Username: username" \ -H "Content-Type: application/json" \ -d '{ "status": "closed", "enabled": "true" }' ``` ### Users API - User Management and Profiles Retrieve and update user information, including profiles, preferences, and activity. ```ruby # File: app/controllers/users_controller.rb class UsersController < ApplicationController def show user = User.find_by_username(params[:username]) guardian.ensure_can_see!(user) serializer = UserSerializer.new( user, scope: guardian, root: false, include_user_status: true ) render_json_dump(serializer) end def update user = User.find(params[:id]) guardian.ensure_can_edit!(user) user_updater = UserUpdater.new(current_user, user) result = user_updater.update( name: params[:name], bio_raw: params[:bio_raw], website: params[:website], location: params[:location] ) if result render_serialized(user, UserSerializer) else render json: { errors: user.errors.full_messages }, status: :unprocessable_entity end end def create user = User.new( email: params[:email], username: params[:username], name: params[:name], password: params[:password] ) if user.save render_serialized(user, UserSerializer) else render json: { errors: user.errors.full_messages }, status: :unprocessable_entity end end end # API Usage Example # GET /u/:username.json curl "https://discourse.example.com/u/john_doe.json" \ -H "Api-Key: YOUR_API_KEY" \ -H "Api-Username: admin" # Response { "user": { "id": 789, "username": "john_doe", "name": "John Doe", "avatar_template": "/user_avatar/discourse.example.com/john_doe/{size}/123.png", "email": "john@example.com", "trust_level": 2, "moderator": false, "admin": false, "created_at": "2024-01-01T00:00:00.000Z", "bio_cooked": "Developer and community enthusiast
", "website": "https://johndoe.com", "location": "San Francisco, CA" } } # PUT /u/:username.json curl -X PUT "https://discourse.example.com/u/john_doe.json" \ -H "Api-Key: YOUR_API_KEY" \ -H "Api-Username: john_doe" \ -H "Content-Type: application/json" \ -d '{ "name": "John Q. Doe", "bio_raw": "Senior developer and open source contributor", "website": "https://johnqdoe.com", "location": "Seattle, WA" }' # POST /users.json (admin only) curl -X POST "https://discourse.example.com/users.json" \ -H "Api-Key: YOUR_API_KEY" \ -H "Api-Username: admin" \ -H "Content-Type: application/json" \ -d '{ "email": "newuser@example.com", "username": "newuser", "name": "New User", "password": "secure_password_123", "active": true }' ``` ### Guardian Authorization - Permission System Centralized authorization system that controls user access to resources and actions. ```ruby # File: lib/guardian.rb class Guardian def initialize(user = nil, request = nil) @user = user.presence || Guardian::AnonymousUser.new @request = request end def authenticated? @user.present? end def is_admin? @user.admin? end def is_staff? @user.staff? end def ensure_can_see!(obj) raise Discourse::InvalidAccess unless can_see?(obj) end def ensure_can_edit!(obj) raise Discourse::InvalidAccess unless can_edit?(obj) end end # File: lib/guardian/post_guardian.rb module PostGuardian def can_see_post?(post) return false if post.nil? return false if post.deleted_at && !can_see_deleted_post?(post) return false unless can_see_topic?(post.topic) true end def can_edit_post?(post) return false if @user.blank? return false if post.topic&.archived? return false if post.user_deleted? && !is_staff? return true if is_admin? return true if is_moderator? && post.topic&.category&.reviewable_by_moderators? return true if post.user_id == @user.id && can_edit_own_post?(post) false end def can_delete_post?(post) return false if post.nil? || @user.blank? return true if is_staff? # Regular users can delete their own posts within time limit if post.user_id == @user.id return false if post.post_number == 1 && post.topic.posts_count > 1 return false if post.created_at < SiteSetting.post_edit_time_limit.seconds.ago return true end false end end # Usage in Controllers class PostsController < ApplicationController def update post = Post.find(params[:id]) # Raises Discourse::InvalidAccess if unauthorized guardian.ensure_can_edit!(post) # Proceed with update revisor = PostRevisor.new(post, current_user) revisor.revise!(current_user, params[:post]) end def destroy post = Post.find(params[:id]) if guardian.can_delete_post?(post) PostDestroyer.new(current_user, post).destroy render json: success_json else raise Discourse::InvalidAccess end end end # Example: Custom Guardian Extension # In a plugin or lib file module CustomGuardian def can_access_private_feature? return false unless authenticated? @user.in_any_groups?(SiteSetting.private_feature_allowed_groups_map) end end # Register the extension Guardian.class_eval do include CustomGuardian end ``` ### Service Objects - Business Logic Encapsulation Service objects extract complex business logic from controllers into reusable, testable components. ```ruby # File: app/services/post_alerter.rb class PostAlerter USER_BATCH_SIZE = 100 def self.post_created(post, opts = {}) PostAlerter.new(opts).after_save_post(post, true) post end def after_save_post(post, new_record = false) return if post.topic.blank? return if !post.topic.visible? if new_record notify_users_after_post_creation(post) notify_group_summary(post) end notify_edit_watchers(post) unless new_record end def notify_users_after_post_creation(post) notify_mentioned_users(post) notify_quoted_users(post) notify_group_mentions(post) notify_watching_users(post) end def notify_mentioned_users(post) mentions = extract_mentions(post) mentions.each do |username| user = User.find_by_username(username) next if user.nil? || user.id == post.user_id create_notification( user: user, notification_type: Notification.types[:mentioned], post: post ) end end private def create_notification(user:, notification_type:, post:) return if user.suspended? Notification.create!( notification_type: notification_type, user_id: user.id, topic_id: post.topic_id, post_number: post.post_number, data: { topic_title: post.topic.title }.to_json ) MessageBus.publish( "/notification-alert/#{user.id}", { notification_type: notification_type, post_number: post.post_number, topic_title: post.topic.title }, user_ids: [user.id] ) end end # Usage Example in Controller class PostsController < ApplicationController def create post = Post.create!( user: current_user, topic_id: params[:topic_id], raw: params[:raw] ) # Delegate notification logic to service PostAlerter.post_created(post) render_serialized(post, PostSerializer) end end # Example: Creating a Custom Service # File: app/services/topic_merger.rb class TopicMerger def initialize(destination_topic, source_topic, user) @destination = destination_topic @source = source_topic @user = user end def merge! raise "Cannot merge topics from different categories" unless compatible? ActiveRecord::Base.transaction do move_posts update_user_actions transfer_topic_users close_source_topic log_staff_action end { success: true, destination_topic_id: @destination.id } rescue => e { success: false, error: e.message } end private def compatible? @destination.category_id == @source.category_id end def move_posts @source.posts.each do |post| post.update!(topic_id: @destination.id) end @destination.reload @destination.update_statistics end def close_source_topic TopicStatusUpdater.new(@source, @user).update!("closed", true) end end # Usage destination = Topic.find(123) source = Topic.find(456) result = TopicMerger.new(destination, source, current_user).merge! if result[:success] render json: success_json.merge(topic_id: result[:destination_topic_id]) else render json: { error: result[:error] }, status: :unprocessable_entity end ``` ### Site Settings - Configuration Management Centralized configuration system accessible throughout the application. ```yaml # File: config/site_settings.yml required: title: client: true default: "Discourse" site_description: default: "" contact_email: default: "noreply@unconfigured.discourse.org" type: email client: true min_post_length: default: 20 min: 1 max: 500 client: true max_post_length: default: 32000 min: 5000 max: 99000000 client: true allow_duplicate_topic_titles: default: false post_undo_action_window_mins: default: 10 client: true enable_mentions: default: true client: true max_mentions_per_post: default: 10 min: 0 posting: min_first_post_length: default: 20 min: 1 max: 500 client: true min_trust_to_post_links: default: 0 enum: "TrustLevelSetting" client: true newuser_max_links: default: 2 client: true newuser_max_images: default: 1 client: true ``` ```ruby # Accessing Site Settings in Ruby class PostsController < ApplicationController def create raw_content = params[:raw] # Validate length if raw_content.length < SiteSetting.min_post_length return render json: { errors: ["Post must be at least #{SiteSetting.min_post_length} characters"] }, status: :unprocessable_entity end if raw_content.length > SiteSetting.max_post_length return render json: { errors: ["Post cannot exceed #{SiteSetting.max_post_length} characters"] }, status: :unprocessable_entity end # Check mentions limit mentions = extract_mentions(raw_content) if mentions.count > SiteSetting.max_mentions_per_post return render json: { errors: ["Cannot mention more than #{SiteSetting.max_mentions_per_post} users"] }, status: :unprocessable_entity end post = Post.create!(user: current_user, raw: raw_content) render_serialized(post, PostSerializer) end end # Accessing in Models class Post < ActiveRecord::Base validate :check_length def check_length if raw.length < SiteSetting.min_post_length errors.add(:raw, "is too short") end end end ``` ```javascript // Accessing Site Settings in JavaScript/Ember // File: frontend/discourse/app/components/composer-body.js import Component from "@ember/component"; import { service } from "@ember/service"; export default class ComposerBody extends Component { @service siteSettings; get minLength() { return this.siteSettings.min_post_length; } get maxLength() { return this.siteSettings.max_post_length; } validateContent() { const content = this.model.reply; if (content.length < this.siteSettings.min_post_length) { this.flashNotice = `Post must be at least ${this.siteSettings.min_post_length} characters`; return false; } if (content.length > this.siteSettings.max_post_length) { this.flashNotice = `Post cannot exceed ${this.siteSettings.max_post_length} characters`; return false; } return true; } } ``` ```ruby # Plugin Site Settings # File: plugins/discourse-solved/config/settings.yml plugins: solved_enabled: default: false client: true allow_solved_on_all_topics: default: false accept_all_solutions_trust_level: default: 4 enum: "TrustLevelSetting" empty_box_on_unsolved: default: false client: true # Access in Plugin Code # File: plugins/discourse-solved/plugin.rb after_initialize do TopicView.class_eval do def can_accept_answer? return false unless SiteSetting.solved_enabled return true if @user&.admin? trust_level = SiteSetting.accept_all_solutions_trust_level @user&.has_trust_level?(trust_level) end end end ``` ### Ember Routes - Frontend Navigation Route definitions handle URL routing and data loading for the Ember.js frontend. ```javascript // File: frontend/discourse/app/routes/topic.js import { action } from "@ember/object"; import { cancel, schedule } from "@ember/runloop"; import { service } from "@ember/service"; import DiscourseRoute from "discourse/routes/discourse"; import Topic from "discourse/models/topic"; export default class TopicRoute extends DiscourseRoute { @service composer; @service screenTrack; @service currentUser; @service router; queryParams = { filter: { replace: true }, username_filters: { replace: true }, }; async model(params) { const topic = await Topic.find(params.id, { page: params.page, post_number: params.post_number, }); return topic; } afterModel(model) { if (!model.can_see) { this.router.replaceWith("/404"); return; } this.screenTrack.start(model.id); } setupController(controller, model) { super.setupController(controller, model); controller.setProperties({ model: model, editingTopic: false, }); } @action showTopicInvite(topic) { this.modal.show(TopicInviteModal, { model: { topic } }); } @action didTransition() { this.controllerFor("topic").send("readPosts"); return true; } } // File: frontend/discourse/app/routes/discovery.js import DiscourseRoute from "discourse/routes/discourse"; import { service } from "@ember/service"; export default class DiscoveryRoute extends DiscourseRoute { @service router; @service currentUser; queryParams = { order: { refreshModel: true }, ascending: { refreshModel: true }, }; async model(params) { const category = params.category_slug_path_with_id ? await this.store.findCategory(params.category_slug_path_with_id) : null; const list = await this.store.findFiltered("topicList", { filter: params.filter || "latest", params: { category: category?.id, order: params.order, ascending: params.ascending, }, }); return { category: category, list: list, }; } titleToken() { const filterText = this.get("filterMode") || "topics"; return filterText.capitalize(); } } // Usage in Templates // File: frontend/discourse/app/templates/topic.hbs