How Docuseal Template Folders Work: Organization and Hierarchy Management
Docuseal uses a self-referencing hierarchy of template folders scoped to accounts, with a default "Default" folder and a service module (TemplateFolders) that handles creation, nesting, and assignment via the "Parent / Child" naming convention.
Docuseal organizes electronic document templates within a flexible folder system that supports arbitrary nesting levels. Each account maintains its own tree of template folders, with every template belonging to exactly one folder (falling back to a system-generated default). This article examines the database schema, service logic, and controller implementations that power folder management in the open-source Docuseal repository.
Database Architecture and Relationships
The folder system rests on three core Active Record associations defined across app/models/account.rb, app/models/template_folder.rb, and app/models/template.rb.
Account-Level Folder Ownership
Each Account maintains its own folder namespace through the following associations:
has_many :template_folders– Standard collection of all folders owned by the account.has_one :default_template_folder– A lazy-loaded special folder named Default (matching the constantTemplateFolder::DEFAULT_NAME) created automatically when first accessed.
The default folder is instantiated in Account#default_template_folder (lines 68-71 of app/models/account.rb) and serves as the catch-all location for uncategorized templates.
Self-Referencing Folder Hierarchy
TemplateFolder implements a tree structure through self-referential associations:
belongs_to :account– Ensures folder isolation between accounts.belongs_to :parent_folder, class_name: 'TemplateFolder', optional: true– Enables arbitrary depth nesting.
Database migrations 20230920202947_add_parent_folder_id_to_template_folders.rb and 20230922070555_add_folder_id_to_templates.rb established the foreign keys required for this hierarchy (parent_folder_id and folder_id respectively).
Template-to-Folder Assignment
The Template model links to folders through belongs_to :folder, class_name: 'TemplateFolder', optional: true (line 94 of app/models/template.rb). While the database column folder_id is nullable, initialization callbacks ensure a folder is always assigned:
self.folder ||= account.default_template_folder
This guarantees every template resides in a folder, even if the user did not explicitly select one during creation.
The TemplateFolders Service Module
All business logic for folder manipulation lives in lib/template_folders.rb. This module provides query helpers, sorting routines, and the primary factory method for folder creation.
Creating Nested Folders by Path
The TemplateFolders.find_or_create_by_name method accepts a user (author), an account, and a string name that may include a path separator (/):
module TemplateFolders
module_function
def find_or_create_by_name(author, name)
return author.account.default_template_folder if name.blank? || name == TemplateFolder::DEFAULT_NAME
parent_name, child_name = name.to_s.split(' / ', 2).map(&:squish)
parent_folder = if child_name.present?
author.account.template_folders.create_with(author:)
.find_or_create_by(name: parent_name, parent_folder_id: nil)
end
author.account.template_folders.create_with(author:)
.find_or_create_by(name: child_name || parent_name, parent_folder: parent_folder)
end
end
Key behaviors:
- Blank or default names immediately return the account's default folder.
- Hierarchical parsing splits strings like
"Marketing / Campaigns"to locate or create the parent first, then the child. - Idempotency uses
find_or_create_byto prevent duplicate folders at the same level.
Supporting Query Methods
The module also exposes search (case-insensitive folder matching), sort (ordering logic for UI display), and scopes like active used by controllers to render folder trees efficiently.
Controller Actions and UI Integration
The folder interface is exposed through TemplateFoldersController and TemplateFoldersAutocompleteController, both delegating heavy lifting to the service module.
Displaying Folder Contents
The TemplateFoldersController#show action (lines 11-22 of app/controllers/template_folders_controller.rb) handles folder display:
- Loads the target folder via
TemplateFolder.find(params[:id]). - Calls
TemplateFolders.searchandTemplateFolders.sortto filter templates based on UI query parameters. - Falls back to displaying only sub-folders when no templates match the current filter.
This design keeps the database queries centralized in the service layer while the controller manages pagination and view rendering.
Renaming and Protection
The update action explicitly guards the default folder:
def update
@template_folder = current_account.template_folders.find(params[:id])
return head(:forbidden) if @template_folder.default?
@template_folder.update!(name: params[:name])
# ...
end
Attempting to rename the Default folder returns HTTP 403, preserving the integrity of the fallback location.
Live Search Endpoint
TemplateFoldersAutocompleteController provides JSON responses for type-ahead folder selection in the UI. It delegates to TemplateFolders.search, allowing users to locate deeply nested folders without traversing the entire tree manually.
Template Assignment and Defaults
Templates acquire their folder assignments through multiple touchpoints, ensuring consistency across creation methods.
Creation via API Tools
When templates are generated programmatically through lib/mcp/tools/create_template.rb, the service explicitly passes folder: account.default_template_folder unless another folder object is supplied. This mirrors the model-level callback behavior.
Cloning Operations
The clone service (lib/templates/clone.rb, lines 8-22) preserves the original folder unless the caller provides a folder_name parameter. When a new name is provided, it invokes TemplateFolders.find_or_create_by_name to resolve or instantiate the target hierarchy:
folder = if folder_name.present?
TemplateFolders.find_or_create_by_name(author, folder_name)
else
template.folder
end
Cascade Deletion
TemplateFolder associations include dependent: :destroy for both templates and subfolders. Deleting a parent folder recursively removes all nested content, preventing orphaned records.
Code Examples
Creating a Nested Folder Structure
# Create "Marketing" with child "Campaigns"
folder = TemplateFolders.find_or_create_by_name(
current_user,
"Marketing / Campaigns"
)
puts folder.full_name
# => "Marketing / Campaigns"
puts folder.parent_folder.name
# => "Marketing"
Moving a Template to a Different Folder
template = Template.find(params[:id])
target_folder = TemplateFolders.find_or_create_by_name(
current_user,
"Legal / Contracts"
)
template.update!(folder: target_folder)
Listing Active Folders for an Account
current_user.account.template_folders.active.each do |folder|
puts "#{folder.id}: #{folder.full_name}"
end
Fetching Folders via Frontend Autocomplete
fetch('/template_folders_autocomplete?term=Marketing')
.then(response => response.json())
.then(folders => {
folders.forEach(folder => {
console.log(folder.name, folder.id);
});
});
Summary
- Template folders in Docuseal form a self-referencing tree structure scoped to individual accounts via
parent_folder_id. - Every account maintains a protected Default folder that catch-uncategorized templates and cannot be renamed or deleted.
- The
TemplateFoldersservice module handles path parsing (using/as delimiter), creation, and querying, keeping controllers thin. - Templates always belong to a folder due to initialization callbacks in
app/models/template.rband defaults set in creation services. - Deletion cascades through
dependent: :destroyassociations, ensuring subfolders and templates are cleaned up when a parent is removed.
Frequently Asked Questions
Can template folders be nested indefinitely?
Yes. The TemplateFolder model uses a self-referential belongs_to :parent_folder association with an optional foreign key (parent_folder_id). The database schema imposes no hard limit on nesting depth, allowing you to create arbitrary hierarchies like "Client / 2024 / Q1 / Contracts" through the TemplateFolders.find_or_create_by_name method.
What happens if I don't specify a folder when creating a template?
The template automatically assigns to the account's Default folder. This occurs in two places: the CreateTemplate service (lib/mcp/tools/create_template.rb) explicitly passes account.default_template_folder, and the Template model callback (app/models/template.rb) executes self.folder ||= account.default_template_folder during initialization.
Can I rename or delete the Default folder?
No. The TemplateFoldersController#update action checks @template_folder.default? and returns HTTP 403 Forbidden if you attempt to rename it. Similarly, the default folder is protected from deletion because the TemplateFolder::DEFAULT_NAME constant is used as a fallback throughout the codebase when folder names are blank.
What happens to templates when their folder is deleted?
All templates within the deleted folder are destroyed along with any nested subfolders. The TemplateFolder model defines has_many :templates, dependent: :destroy and has_many :subfolders, class_name: 'TemplateFolder', dependent: :destroy. This cascade ensures that removing a parent folder purges its entire contents, preventing orphaned template records.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →