diff --git a/.github/scripts/__tests__/inactive-issues.test.js b/.github/scripts/__tests__/inactive-issues.test.js
new file mode 100644
index 0000000000..91f9ddf60f
--- /dev/null
+++ b/.github/scripts/__tests__/inactive-issues.test.js
@@ -0,0 +1,198 @@
+// Tests for inactive-issues.js using Node's built-in test runner (node:test)
+// We only care about the last comments (per user instruction), so we don't need pagination logic.
+
+const { test, beforeEach } = require('node:test');
+const assert = require('node:assert');
+const path = require('node:path');
+
+// Load the script dynamically so we can pass mocks.
+const scriptPath = path.join(__dirname, '..', 'inactive-issues.js');
+
+// Helper to advance days
+const daysAgo = (days) => {
+ const d = new Date();
+ d.setUTCDate(d.getUTCDate() - days);
+ return d.toISOString();
+};
+
+// Factory for github REST mock structure
+function buildGithubMock({ issues, commentsByIssue }) {
+ return {
+ rest: {
+ issues: {
+ listForRepo: async ({ page, per_page }) => {
+ // simple pagination slice
+ const start = (page - 1) * per_page;
+ const end = start + per_page;
+ return { data: issues.slice(start, end) };
+ },
+ listComments: async ({ issue_number }) => {
+ return { data: commentsByIssue[issue_number] || [] };
+ },
+ createComment: async ({ issue_number, body }) => {
+ // push comment to structure to allow assertions on side effects if needed
+ const arr = commentsByIssue[issue_number] || (commentsByIssue[issue_number] = []);
+ arr.push({
+ id: Math.random(),
+ body,
+ created_at: new Date().toISOString(),
+ user: { login: 'github-actions[bot]', type: 'Bot' }
+ });
+ return {};
+ },
+ addLabels: async ({ issue_number, labels }) => {
+ const issue = issues.find(i => i.number === issue_number);
+ if (issue) {
+ (issue.labels || (issue.labels = [])).push(...labels.map(l => ({ name: l })));
+ }
+ return {};
+ }
+ }
+ }
+ };
+}
+
+// Wrap script invocation for reuse
+async function runScript({ issues, commentsByIssue }) {
+ delete require.cache[require.resolve(scriptPath)];
+ const fn = require(scriptPath);
+ const github = buildGithubMock({ issues, commentsByIssue });
+ await fn({ github, context: { repo: { owner: 'meteor', repo: 'meteor' } } });
+ return { issues, commentsByIssue };
+}
+
+let baseIssueNumber = 1000;
+function makeIssue({ daysSinceHumanActivity, isPR = false, labels = [], user = 'user1' }) {
+ // We'll simulate by setting created_at to the human activity date if no comments.
+ const updated_at = daysAgo(daysSinceHumanActivity);
+ return {
+ number: baseIssueNumber++,
+ pull_request: isPR ? {} : undefined,
+ labels: labels.map(n => ({ name: n })),
+ user: { login: user },
+ created_at: daysAgo(daysSinceHumanActivity),
+ updated_at
+ };
+}
+
+// TESTS
+
+beforeEach(() => {
+ baseIssueNumber = 1000;
+});
+
+test('60 days inactivity -> adds reminder comment (no prior reminder)', async () => {
+ const issue = makeIssue({ daysSinceHumanActivity: 60 });
+ const issues = [issue];
+ const commentsByIssue = { [issue.number]: [] }; // no comments
+
+ await runScript({ issues, commentsByIssue });
+
+ const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]');
+ assert.equal(botComments.length, 1, 'Should have 1 reminder comment');
+ assert.match(botComments[0].body, /60 days/);
+ assert.ok(!issue.labels.some(l => l.name === 'idle'), 'Should not label yet');
+});
+
+test('60 days inactivity but already reminded -> no duplicate comment', async () => {
+ const issue = makeIssue({ daysSinceHumanActivity: 65 });
+ const issues = [issue];
+ const commentsByIssue = {
+ [issue.number]: [
+ {
+ id: 1,
+ body: 'đź‘‹ @user1 This issue has been open with no human activity for 60 days. Is this issue still relevant? If there is no human response or activity within the next 30 days, this issue will be labeled as `idle`.',
+ created_at: daysAgo(5), // 5 days ago bot comment (means last human is 65 days, bot after human)
+ user: { login: 'github-actions[bot]', type: 'Bot' }
+ }
+ ]
+ };
+
+ await runScript({ issues, commentsByIssue });
+
+ const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]');
+ assert.equal(botComments.length, 1, 'Should still have only the existing reminder');
+});
+
+test('90 days inactivity -> label + comment', async () => {
+ const issue = makeIssue({ daysSinceHumanActivity: 95 });
+ const issues = [issue];
+ const commentsByIssue = { [issue.number]: [] };
+
+ await runScript({ issues, commentsByIssue });
+
+ assert.ok(issue.labels.some(l => l.name === 'idle'), 'Should add idle label');
+ const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]');
+ assert.equal(botComments.length, 1, 'Should comment when labeling');
+ assert.match(botComments[0].body, /90 days/i);
+});
+
+test('90 days inactivity but already labeled -> no action', async () => {
+ const issue = makeIssue({ daysSinceHumanActivity: 100, labels: ['idle'] });
+ const issues = [issue];
+ const commentsByIssue = { [issue.number]: [] };
+
+ await runScript({ issues, commentsByIssue });
+
+ const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]');
+ assert.equal(botComments.length, 0, 'Should not comment again');
+});
+
+test('90 days inactivity but already labeled `in-development` -> no action', async () => {
+ const issue = makeIssue({ daysSinceHumanActivity: 100, labels: ['in-development'] });
+ const issues = [issue];
+ const commentsByIssue = { [issue.number]: [] };
+
+ await runScript({ issues, commentsByIssue });
+
+ const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]');
+ assert.equal(botComments.length, 0, 'Should not comment again');
+});
+
+
+test('Human reply after reminder resets cycle (no immediate labeling)', async () => {
+ // Scenario: last human activity 10 days ago, bot commented 40 days ago (which was 50 days after original). Should NOT comment again or label.
+ const issue = makeIssue({ daysSinceHumanActivity: 10 });
+ const issues = [issue];
+ const commentsByIssue = {
+ [issue.number]: [
+ {
+ id: 1,
+ body: 'đź‘‹ @user1 This issue has been open with no human activity for 60 days... ',
+ created_at: daysAgo(50),
+ user: { login: 'github-actions[bot]', type: 'Bot' }
+ },
+ {
+ id: 2,
+ body: 'I am still seeing this problem',
+ created_at: daysAgo(10),
+ user: { login: 'some-human', type: 'User' }
+ }
+ ]
+ };
+
+ await runScript({ issues, commentsByIssue });
+
+ const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]');
+ assert.equal(botComments.length, 1, 'Should not add a new bot comment');
+ assert.ok(!issue.labels.some(l => l.name === 'idle'), 'Should not label');
+});
+
+test('Only bot comments (no human ever) counts from creation date', async () => {
+ const issue = makeIssue({ daysSinceHumanActivity: 61 });
+ const issues = [issue];
+ const commentsByIssue = {
+ [issue.number]: [
+ {
+ id: 1,
+ body: 'Automated maintenance notice',
+ created_at: daysAgo(30),
+ user: { login: 'github-actions[bot]', type: 'Bot' }
+ }
+ ]
+ };
+
+ await runScript({ issues, commentsByIssue });
+ const botComments = commentsByIssue[issue.number].filter(c => /60 days/.test(c.body));
+ assert.equal(botComments.length, 1, 'Should add a 60-day reminder');
+});
diff --git a/.github/scripts/inactive-issues.js b/.github/scripts/inactive-issues.js
index cbca2f31aa..a75f5b81a1 100644
--- a/.github/scripts/inactive-issues.js
+++ b/.github/scripts/inactive-issues.js
@@ -1,190 +1,200 @@
+/**
+* Mark issues as idle after a period of inactivity
+* and post reminders after a shorter period of inactivity.
+*
+* 1. Issues with no human activity for 60 days get a reminder comment.
+* 2. Issues with no human activity for 90 days get labeled as "idle" and get a comment.
+*
+* Human activity is defined as any comment from a non-bot user.
+*
+* This script is intended to be run as a GitHub Action on a schedule (e.g., daily).
+ */
module.exports = async ({ github, context }) => {
const daysToComment = 60;
const daysToLabel = 90;
+
+ const idleTimeComment = daysToComment * 24 * 60 * 60 * 1000;
+ const idleTimeLabel = daysToLabel * 24 * 60 * 60 * 1000;
const now = new Date();
- const idleTimeComment = daysToComment * 24 * 60 * 60 * 1000; // 60 days in milliseconds
- const idleTimeLabel = daysToLabel * 24 * 60 * 60 * 1000; // 90 days in milliseconds
-
- // Function to fetch issues until we find recently updated ones
+
+ const BOT_LOGIN = 'github-actions[bot]';
+ const REMINDER_PHRASE = 'Is this issue still relevant?';
+
+ const COMMENT_60_TEMPLATE = (login) =>
+ `đź‘‹ @${login} This issue has been open with no human activity for ${daysToComment} days. Is this issue still relevant? If there is no human response or activity within the next ${daysToLabel - daysToComment} days, this issue will be labeled as \`idle\`.`;
+
+ const COMMENT_90_TEXT =
+ 'This issue has been automatically labeled as `idle` due to 90 days of inactivity (no human interaction). If this is still relevant or if someone is working on it, please comment or add `in-development` label.';
+
+ // Fetch all open issues
async function fetchAllIssues() {
- let allIssues = [];
let page = 1;
- let hasNextPage = true;
- const now = new Date();
- const minInactivity = idleTimeComment; // 60 days in milliseconds
-
- while (hasNextPage) {
- const response = await github.rest.issues.listForRepo({
+ const per_page = 100;
+ const results = [];
+ let keepGoing = true;
+
+ while (keepGoing) {
+ const { data } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
- per_page: 100,
- page: page,
+ per_page,
+ page,
sort: 'updated',
- direction: 'asc' // Oldest updated first
+ direction: 'asc'
});
-
- // Check if the most recently updated issue on this page is too recent
- let recentIssueFound = false;
- if (response.data.length > 0) {
- // Check the last issue on the page (most recently updated)
- const lastIssue = response.data[response.data.length - 1];
- const lastIssueUpdatedAt = new Date(lastIssue.updated_at);
- const timeSinceLastIssueUpdate = now.getTime() - lastIssueUpdatedAt.getTime();
-
- if (timeSinceLastIssueUpdate < minInactivity) {
- // This page already has issues that are too recent, filter them out
- const filteredIssues = response.data.filter(issue => {
- const issueUpdatedAt = new Date(issue.updated_at);
- const timeSinceUpdate = now.getTime() - issueUpdatedAt.getTime();
- return timeSinceUpdate >= minInactivity;
- });
-
- allIssues = allIssues.concat(filteredIssues);
- recentIssueFound = true;
- hasNextPage = false;
- } else {
- // All issues on this page are old enough, keep them all
- allIssues = allIssues.concat(response.data);
- }
- }
-
- // Stop if we found recent issues or reached the end of pagination
- if (recentIssueFound) {
- hasNextPage = false;
- } else if (response.data.length < 100) {
- hasNextPage = false;
+
+ if (!data.length) break;
+ results.push(...data);
+
+ if (data.length < per_page) {
+ keepGoing = false;
} else {
page++;
- // Small delay to avoid hitting rate limits
- await new Promise(resolve => setTimeout(resolve, 100));
+ await new Promise((r) => setTimeout(r, 120));
}
}
-
- return allIssues;
+ return results;
}
-
- // Fetch all issues
- const allIssues = await fetchAllIssues();
-
- let processedCount = 0;
- let commentedCount = 0;
- let labeledCount = 0;
-
- for (const issue of allIssues) {
- processedCount++;
-
- // Skip pull requests
- if (issue.pull_request) {
- continue;
- }
-
- // Skip issues that already have the idle label
- if (issue.labels.some(label => label.name === 'idle')) {
- continue;
- }
-
- // Get latest comment or update date
- const issueUpdatedAt = new Date(issue.updated_at);
- const timeSinceUpdate = now.getTime() - issueUpdatedAt.getTime();
-
- // Handle 60-day idle issues (comment)
- if (timeSinceUpdate > idleTimeComment && timeSinceUpdate <= idleTimeLabel) {
- // Check if bot already commented to avoid duplicate comments
- const comments = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issue.number,
- per_page: 100
- });
-
- // Check if there's a recent bot comment
- const botCommented = comments.data.some(comment => {
- const commentDate = new Date(comment.created_at);
- const timeSinceComment = now.getTime() - commentDate.getTime();
- const isBot = comment.user.login === 'github-actions[bot]';
- const isRecent = timeSinceComment < idleTimeComment;
- const hasRightContent = comment.body.includes('Is this issue still relevant?');
-
- return isBot && isRecent && hasRightContent;
- });
-
- if (!botCommented) {
- try {
- const result = await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issue.number,
- body: `đź‘‹ @${issue.user.login} This issue has been open for 60 days with no activity. Is this issue still relevant? If there is no response or activity within the next 30 days, this issue will be labeled as \`idle\`.`
- });
- commentedCount++;
- } catch (error) {
- // Add retry logic
- try {
- // Wait for 5 seconds before retrying
- await new Promise(resolve => setTimeout(resolve, 5000));
-
- const retryResult = await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issue.number,
- body: `đź‘‹ @${issue.user.login} This issue has been open for 60 days with no activity. Is this issue still relevant? If there is no response or activity within the next 30 days, this issue will be labeled as \`idle\`.`
- });
- commentedCount++;
- } catch (retryError) {
- // Failed retry, continue with other issues
- }
- }
+ // analyse comments to find last human activity and if a reminder was already posted after that
+ async function analyzeComments(issueNumber, issueCreatedAt) {
+ const commentsResp = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issueNumber,
+ per_page: 100
+ });
+
+ const comments = commentsResp.data;
+ let lastHumanActivity = null;
+
+ for (let i = comments.length - 1; i >= 0; i--) {
+ const c = comments[i];
+ const isBot = c.user?.type === 'Bot' || c.user?.login === BOT_LOGIN;
+ if (!isBot) {
+ lastHumanActivity = new Date(c.created_at);
+ break;
}
}
-
- // Handle 90-day idle issues (add label)
- else if (timeSinceUpdate > idleTimeLabel) {
- // Check if the issue has the idle label
- if (!issue.labels.some(label => label.name === 'idle')) {
+
+ if (!lastHumanActivity) {
+ lastHumanActivity = new Date(issueCreatedAt);
+ }
+
+ const hasReminderAfterLastHuman = comments.some(
+ (c) =>
+ c.user?.login === BOT_LOGIN &&
+ c.body?.includes(REMINDER_PHRASE) &&
+ new Date(c.created_at) > lastHumanActivity
+ );
+
+ return { lastHumanActivity, hasReminderAfterLastHuman };
+ }
+
+ const issues = await fetchAllIssues();
+
+ let processed = 0;
+ let commented = 0;
+ let labeled = 0;
+ let skippedPR = 0;
+
+ for (const issue of issues) {
+ processed++;
+
+ if (issue.pull_request) {
+ skippedPR++;
+ continue;
+ }
+
+ if (issue.labels.some((l) => l.name === 'idle' || l.name === 'in-development')) {
+ continue;
+ }
+
+ let analysis;
+ try {
+ analysis = await analyzeComments(issue.number, issue.created_at);
+ } catch (err) {
+ continue; // fail to get comments, skip
+ }
+
+ const { lastHumanActivity, hasReminderAfterLastHuman } = analysis;
+ const inactivityMs = now.getTime() - lastHumanActivity.getTime();
+
+ // 90+ days => label + comment
+ if (inactivityMs >= idleTimeLabel) {
+ try {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ labels: ['idle']
+ });
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ body: COMMENT_90_TEXT
+ });
+ labeled++;
+ continue;
+ } catch (err) {
+ // retry simples
try {
- // Add the label
- const labelResult = await github.rest.issues.addLabels({
+ await new Promise((r) => setTimeout(r, 5000));
+ await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['idle']
});
-
- // Add a comment when labeling as idle
- const commentResult = await github.rest.issues.createComment({
+ await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
- body: `This issue has been automatically labeled as \`idle\` due to 90 days of inactivity. If this issue is still relevant, please comment to reactivate it.`
+ body: COMMENT_90_TEXT
});
- labeledCount++;
- } catch (error) {
- // Add retry logic with exponential backoff
- try {
- // Wait for 5 seconds before retrying
- await new Promise(resolve => setTimeout(resolve, 5000));
-
- const retryLabelResult = await github.rest.issues.addLabels({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issue.number,
- labels: ['idle']
- });
-
- // Retry adding comment
- const retryCommentResult = await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issue.number,
- body: `This issue has been automatically labeled as \`idle\` due to 90 days of inactivity. If this issue is still relevant, please comment to reactivate it.`
- });
- labeledCount++;
- } catch (retryError) {
- // Continue with other issues if retry fails
- }
- }
+ labeled++;
+ } catch {}
+ continue;
+ }
+ }
+
+ // 60-89 days => comment (once)
+ if (
+ inactivityMs >= idleTimeComment &&
+ inactivityMs < idleTimeLabel &&
+ !hasReminderAfterLastHuman
+ ) {
+ const body = COMMENT_60_TEMPLATE(issue.user.login);
+ try {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ body
+ });
+ commented++;
+ } catch (err) {
+ try {
+ await new Promise((r) => setTimeout(r, 5000));
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issue.number,
+ body
+ });
+ commented++;
+ } catch {}
}
}
}
+
+ // Log summary for CI
+ console.log(
+ JSON.stringify(
+ { processed, commented, labeled, skippedPR },
+ null,
+ 2
+ )
+ );
};
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index beebcdb1ac..c646fad457 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -155,7 +155,7 @@ Learn how we use GitHub labels [here](LABELS.md)
## Documentation
-If you'd like to contribute to Meteor's documentation, head over to https://docs.meteor.com or https://guide.meteor.com and if you find something that could be better click in "Edit on GitHub" footer to edit and submit a PR.
+If you'd like to contribute to Meteor's documentation, head over to https://docs.meteor.com/about/contributing.html for guidelines.
## Blaze
diff --git a/docs/source/commandline.md b/docs/source/commandline.md
index 11138250d5..e1494f5a7c 100644
--- a/docs/source/commandline.md
+++ b/docs/source/commandline.md
@@ -677,7 +677,7 @@ Your project should be a git repository as the commit hash is going to be used t
The `cache-build` option is available since Meteor 1.11.
{% endpullquote %}
-With the argument `--container-size` you can change your app's container size using the deploy command. The valid arguments are: `tiny`, `compact`, `standard`, `double`, `quad`, `octa`, and `dozen`. One more thing to note here is that the `--container-size` flag can only be used when the `--plan` option is already specified, otherwise using the `--container-size` option will throw an error with the message : `Error deploying application: Internal error`. To see more about the difference and prices of each one you can check [here](https://www.meteor.com/cloud#pricing-section).
+With the argument `--container-size` you can change your app's container size using the deploy command. The valid arguments are: `tiny`, `compact`, `standard`, `double`, `quad`, `octa`, and `dozen`. One more thing to note here is that the `--container-size` flag can only be used when the `--plan` option is already specified, otherwise using the `--container-size` option will throw an error with the message : `Error deploying application: Internal error`. To see more about the difference and prices of each one you can check [here](https://galaxycloud.app/meteorjs/pricing).
{% pullquote warning %}
The `--container-size` option is available since Meteor 2.4.1.
diff --git a/guide/source/deployment.md b/guide/source/deployment.md
index c254c1a196..fa9d8f08de 100644
--- a/guide/source/deployment.md
+++ b/guide/source/deployment.md
@@ -194,7 +194,7 @@ MONGO_URL=mongodb://localhost:27017/myapp ROOT_URL=http://my-app.com PORT=3000 n
```
* `ROOT_URL` is the base URL for your Meteor project
-* `PORT` is the port at which the application is running
+* `PORT` is the port at which the application is running
* `MONGO_URL` is a [Mongo connection string URI](https://docs.mongodb.com/manual/reference/connection-string/) supplied by the MongoDB provider.
@@ -322,7 +322,7 @@ Galaxy's UI provides a detailed logging system, which can be invaluable to deter
If you really want to understand the ins and outs of running your Meteor application, you should use an Application Performance Monitoring (APM) service. There are multiple services designed for Meteor apps:
-- [Meteor APM](https://www.meteor.com/cloud)
+- [Meteor APM](https://galaxycloud.app/)
- [Monti APM](https://montiapm.com/)
- [Meteor Elastic APM](https://github.com/Meteor-Community-Packages/meteor-elastic-apm)
diff --git a/guide/source/performance-improvement.md b/guide/source/performance-improvement.md
index f6721550f7..28298e8232 100644
--- a/guide/source/performance-improvement.md
+++ b/guide/source/performance-improvement.md
@@ -3,10 +3,10 @@ title: Performance improvements
description: How to optimize your Meteor application for higher performance when you start growing.
---
-This guide focuses on providing you tips and common practices on how to improve performance of your Meteor app (sometimes also called scaling).
-It is important to note that at the end of the day Meteor is a Node.js app tied closely to MongoDB,
-so a lot of the problems you are going to encounter are common to other Node.js and MongoDB apps.
-Also do note that every app is different so there are unique challenges to each, therefore
+This guide focuses on providing you tips and common practices on how to improve performance of your Meteor app (sometimes also called scaling).
+It is important to note that at the end of the day Meteor is a Node.js app tied closely to MongoDB,
+so a lot of the problems you are going to encounter are common to other Node.js and MongoDB apps.
+Also do note that every app is different so there are unique challenges to each, therefore
practices describe in this guide should be used as a guiding posts rather than absolutes.
This guide has been heavily inspired by [Marcin Szuster's Vazco article](https://www.vazco.eu/blog/how-to-optimize-and-scale-meteor-projects), the official [Meteor Galaxy guide](https://galaxy-guide.meteor.com/),
@@ -15,11 +15,11 @@ and talk by Paulo MogollĂłn's talk at Impact 2022 titled ["First steps on scalin
Performance monitoring
Before any optimization can take place we need to know what is our problem. This is where APM (Application Performance Monitor) comes in.
-If you are hosting on Galaxy then this is automatically included in the [Professional plan](https://www.meteor.com/cloud/pricing)
-and you can learn more about in its [own dedicated guide article](https://cloud-guide.meteor.com/apm-getting-started.html).
-For those hosting outside of Galaxy the most popular solution is to go with [Monti APM](https://montiapm.com/) which shares
-all the main functionality with Galaxy APM. You can also choose other APM for Node.js, but they will not show you Meteor
-specific data that Galaxy APM and Monti APM specialize in. For this guide we will focus on showing how to work with Galaxy APM,
+If you are hosting on Galaxy then this is automatically included in the [Professional plan](https://galaxycloud.app/meteorjs/pricing)
+and you can learn more about in its [own dedicated guide article](https://cloud-guide.meteor.com/apm-getting-started.html).
+For those hosting outside of Galaxy the most popular solution is to go with [Monti APM](https://montiapm.com/) which shares
+all the main functionality with Galaxy APM. You can also choose other APM for Node.js, but they will not show you Meteor
+specific data that Galaxy APM and Monti APM specialize in. For this guide we will focus on showing how to work with Galaxy APM,
which is the same as with Monti APM, for simplicity.
Once you setup either of those APMs you will need to add a package to your Meteor app to start sending them data.
@@ -37,9 +37,9 @@ meteor add montiapm:agent
```
Finding issues in APM
-APM will start with providing you with an overview of how your app is performing. You can then dive deep into details of
-publications, methods, errors happening (both on client and server) and more. You will spend a lot of time in the detailed
-tabs looking for methods and publications to improve and analyzing the impact of your actions. The process, for example for
+APM will start with providing you with an overview of how your app is performing. You can then dive deep into details of
+publications, methods, errors happening (both on client and server) and more. You will spend a lot of time in the detailed
+tabs looking for methods and publications to improve and analyzing the impact of your actions. The process, for example for
optimizing methods, will look like this:
1. Go to the detailed view under the Methods tab.
@@ -52,14 +52,14 @@ Not every long-performing method has to be improved. Take a look at the followin
* methodX - mean response time 1 515 ms, throughput 100,05/min
* methodY - mean response time 34 000 ms, throughput 0,03/min
-At first glance, the 34 seconds response time can catch your attention, and it may seem that the methodY
-is more relevant to improvement. But don’t ignore the fact that this method is being used only once in
+At first glance, the 34 seconds response time can catch your attention, and it may seem that the methodY
+is more relevant to improvement. But don’t ignore the fact that this method is being used only once in
a few hours by the system administrators or scheduled cron action.
-And now, let’s take a look at the methodX. Its response time is evidently lower BUT compared to the frequency
+And now, let’s take a look at the methodX. Its response time is evidently lower BUT compared to the frequency
of use, it is still high, and without any doubt should be optimized first.
-It’s also absolutely vital to remember that you shouldn't optimize everything as it goes.
+It’s also absolutely vital to remember that you shouldn't optimize everything as it goes.
The key is to think strategically and match the most critical issues with your product priorities.
For more information about all the things you can find in Galaxy APM take a look at the Meteor APM section in [Galaxy Guide](https://galaxy-guide.meteor.com/apm-getting-started.html).
@@ -71,75 +71,75 @@ At the same this is the most resource intensive part of a Meteor application.
Under the hood WebSockets are being used with additional abilities provided by DDP.
Proper use of publications
-Since publications can get resource intensive they should be reserved for usage that requires up to date, live data or
+Since publications can get resource intensive they should be reserved for usage that requires up to date, live data or
that are changing frequently and you need the users to see that.
-You will need to evaluate your app to figure out which situations these are. As a rule of thumb any data that are not
-required to be live or are not changing frequently can be fetched once via other means and re-fetched as needed,
+You will need to evaluate your app to figure out which situations these are. As a rule of thumb any data that are not
+required to be live or are not changing frequently can be fetched once via other means and re-fetched as needed,
in most cases the re-fetching shouldn't be necessary.
-But even before you proceed any further there are a few improvements that you can make here.
+But even before you proceed any further there are a few improvements that you can make here.
First make sure that you only get the fields you need, limit the number of documents you send to the client to what you need
(aka always set the `limit` option) and ensure that you have set all your indexes.
Methods over publications
-The first easiest replacement is to use Meteor methods instead of publications. In this case you can use the existing publication
-and instead of returning a cursor you will call `.fetchAsync()` and return the actual data. The same performance improvements
+The first easiest replacement is to use Meteor methods instead of publications. In this case you can use the existing publication
+and instead of returning a cursor you will call `.fetchAsync()` and return the actual data. The same performance improvements
to get the method work faster apply here, but once called it sends the data and you don't have the overhead of a publication.
-What is crucial here is to ensure that your choice of a front-end framework doesn't call the method every time, but only once
+What is crucial here is to ensure that your choice of a front-end framework doesn't call the method every time, but only once
to load the data or when specifically needed (for example when the data gets updated due to user action or when the user requests it).
Publication replacements
Using methods has its limitations and there are other tools that you might want to evaluate as a potential replacement.
-[Grapher](https://github.com/cult-of-coders/grapher) is a favorite answer and allows you to easily blend with another
-replacement which is [GraphQL](https://graphql.org/) and in particular [Apollo GraphQL](https://www.apollographql.com/),
+[Grapher](https://github.com/cult-of-coders/grapher) is a favorite answer and allows you to easily blend with another
+replacement which is [GraphQL](https://graphql.org/) and in particular [Apollo GraphQL](https://www.apollographql.com/),
which also has an integration [package](https://atmospherejs.com/meteor/apollo) with Meteor. Finally, you can also go back to using REST as well.
Do note, that you can mix all of these based on your needs.
Low observer reuse
-Observers are among the key components of Meteor. They take care of observing documents on MongoDB and they notify changes.
+Observers are among the key components of Meteor. They take care of observing documents on MongoDB and they notify changes.
Creating them is an expensive operations, so you want to make sure that Meteor reuses them as much as possible.
> [Learn more about observers](https://galaxy-guide.meteor.com/apm-know-your-observers.html)
-The key for observer reuse is to make sure that the queries requested are identical. This means that user given values
-should be standardised and so should any dynamic input like time. Publications for users should check if user is signed in
+The key for observer reuse is to make sure that the queries requested are identical. This means that user given values
+should be standardised and so should any dynamic input like time. Publications for users should check if user is signed in
first before returning publication and if user is not signed in, then it should instead call `this.ready();`.
> [Learn more on improving observer reuse](https://galaxy-guide.meteor.com/apm-improve-cpu-and-network-usage)
Redis Oplog
-[Redis Oplog](https://atmospherejs.com/cultofcoders/redis-oplog) is a popular solution to Meteor's Oplog tailing
-(which ensures the reactivity, but has some severe limitations that especially impact performance). Redis Oplog as name
-suggests uses [redis](https://redis.io/) to track changes to data that you only need and cache them. This reduces load on
+[Redis Oplog](https://atmospherejs.com/cultofcoders/redis-oplog) is a popular solution to Meteor's Oplog tailing
+(which ensures the reactivity, but has some severe limitations that especially impact performance). Redis Oplog as name
+suggests uses [redis](https://redis.io/) to track changes to data that you only need and cache them. This reduces load on
the server and database, allows you to track only the data that you want and only publish the changes you need.
Methods
-While methods are listed as one of the possible replacements for publications, they themselves can be made more performant,
-after all it really depends on what you put inside them and APM will provide you with the necessary insight on which
+While methods are listed as one of the possible replacements for publications, they themselves can be made more performant,
+after all it really depends on what you put inside them and APM will provide you with the necessary insight on which
methods are the problem.
Heavy actions
-In general heavy tasks that take a lot of resources or take long and block the server for that time should be taken out
-and instead be run in its own server that focuses just on running those heavy tasks. This can be another Meteor server
+In general heavy tasks that take a lot of resources or take long and block the server for that time should be taken out
+and instead be run in its own server that focuses just on running those heavy tasks. This can be another Meteor server
or even better something specifically optimized for that given task.
Reoccurring jobs
-Reoccurring jobs are another prime candidate to be taken out into its own application. What this means is that you will have
-an independent server that is going to be tasked with running the reoccurring jobs and the main application will only add to
+Reoccurring jobs are another prime candidate to be taken out into its own application. What this means is that you will have
+an independent server that is going to be tasked with running the reoccurring jobs and the main application will only add to
the list and be recipient of the results, most likely via database results.
Rate limiting
-Rate limit your methods to reduce effectiveness of DDOS attack and spare your server. This is also a good practice to
-ensure that you don't accidentally DDOS your self. For example a user who clicks multiple time on a button that triggers
-an expensive function. In this example you should also in general ensure that any button that triggers a server event
+Rate limit your methods to reduce effectiveness of DDOS attack and spare your server. This is also a good practice to
+ensure that you don't accidentally DDOS your self. For example a user who clicks multiple time on a button that triggers
+an expensive function. In this example you should also in general ensure that any button that triggers a server event
should be disabled until there is a response from the server that the event has finished.
You can and should rate limit both methods and collections.
@@ -154,15 +154,15 @@ These are all applicable, and you should spend some time researching into them a
IP whitelisting
-If your MongoDB hosting provider allows it, you should make sure that you whitelist the IPs of your application servers.
-If you don't then your database servers are likely to come under attack from hackers trying to brute force their way in.
+If your MongoDB hosting provider allows it, you should make sure that you whitelist the IPs of your application servers.
+If you don't then your database servers are likely to come under attack from hackers trying to brute force their way in.
Besides the security risk this also impacts performance as authentication is not a cheap operation and it will impact performance.
See [Galaxy guide](https://galaxy-guide.meteor.com/container-environment.html#network-outgoing) on IP whitelisting to get IPs for your Galaxy servers.
Indexes
-While single indexes on one field are helpful on simple query calls, you will most likely have more advance queries with
+While single indexes on one field are helpful on simple query calls, you will most likely have more advance queries with
multiple variables. To cover those you will need to create compound indexes. For example:
```javascript
@@ -177,7 +177,7 @@ Statistics.createIndexAsync(
```
When creating indexes you should sort the variables in ESR (equity, sort, range) style.
Meaning, first you put variables that will be equal to something specific. Second you put variables that sort things,
-and third variables that provide range for that query.
+and third variables that provide range for that query.
Further you should order these variables in a way that the fields that filter the most should be first.
Make sure that all the indexes are used and remove unused indexes as leaving unused indexes will have negative impact
@@ -187,7 +187,7 @@ on performance as the database will have to still keep track on all the indexed
To optimize finds ensure that all queries have are indexed. Meaning that any `.find()` variables should be indexed as described above.
-All your finds should have a limit on the return so that the database stops going through the data once it has reached
+All your finds should have a limit on the return so that the database stops going through the data once it has reached
the limit, and you only return the limited number of results instead of the whole database.
Beware of queries with `n + 1` issue. For example in a database that has cars and car owners. You don't want to get cars,
@@ -202,14 +202,14 @@ If you still have issues make sure that you read data from secondaries.
Beware of collection hooks
-While collection hooks can help in many cases beware of them and make sure that you understand how they work as they might
-create additional queries that you might not know about. Make sure to review packages that use them so that they won't
+While collection hooks can help in many cases beware of them and make sure that you understand how they work as they might
+create additional queries that you might not know about. Make sure to review packages that use them so that they won't
create additional queries.
Caching
Once your user base increases you want to invest into query caching like using Redis, Redis Oplog and other.
-For more complex queries or when you are retrieving data from multiple collections, then you want to use [aggregation](https://www.mongodb.com/docs/manual/aggregation/)
+For more complex queries or when you are retrieving data from multiple collections, then you want to use [aggregation](https://www.mongodb.com/docs/manual/aggregation/)
and save their results.
Scaling
@@ -229,13 +229,13 @@ Galaxy has these as well. Learn more about [setting triggers for scaling on Gala
Setting this is vital, so that your application can keep on running when you have extra people come and then saves you money by scaling down when the containers are not in use.
When initially setting these pay a close attention to the performance of your app. you need to learn when is the right time to scale your app so it has enough time to spin up new containers before the existing one get overwhelmed by traffic and so on.
-There are other points to pay attention to as well. For example if your app is used by corporation you might want to setup that on weekdays the minimum number of containers is going to increase just before the start of working hours and the then decrease the minimum to 1 for after hours and on weekends.
+There are other points to pay attention to as well. For example if your app is used by corporation you might want to setup that on weekdays the minimum number of containers is going to increase just before the start of working hours and the then decrease the minimum to 1 for after hours and on weekends.
Usually when you are working on performance issues you will have higher numbers of containers as you optimize your app. It is therefore vital to revisit your scaling setting after each rounds of improvements to ensure that scaling triggers are properly optimized.
@@ -398,7 +395,6 @@ even if the method's writes are not available yet, you can specify an
Use `Meteor.call` only to call methods that do not have a stub, or have a sync stub. If you want to call methods with an async stub, `Meteor.callAsync` can be used with any method.
:::
-
`Meteor.callAsync` is just like `Meteor.call`, except that it'll return a promise that you need to solve to get the server result. Along with the promise returned by `callAsync`, you can also handle `stubPromise` and `serverPromise` for managing client-side simulation and server response.
@@ -409,64 +405,63 @@ The following sections guide you in understanding these promises and how to mana
```javascript
try {
- await Meteor.callAsync('greetUser', 'John');
- // 🟢 Server ended with success
-} catch(e) {
- console.error("Error:", error.reason); // đź”´ Server ended with error
+ await Meteor.callAsync("greetUser", "John");
+ // 🟢 Server ended with success
+} catch (e) {
+ console.error("Error:", error.reason); // đź”´ Server ended with error
}
-Greetings.findOne({ name: 'John' }); // 🗑️ Data is NOT available
+Greetings.findOne({ name: "John" }); // 🗑️ Data is NOT available
```
#### stubPromise
```javascript
-await Meteor.callAsync('greetUser', 'John').stubPromise;
+await Meteor.callAsync("greetUser", "John").stubPromise;
// 🔵 Client simulation
-Greetings.findOne({ name: 'John' }); // đź§ľ Data is available (Optimistic-UI)
+Greetings.findOne({ name: "John" }); // đź§ľ Data is available (Optimistic-UI)
```
#### stubPromise and serverPromise
```javascript
-const { stubPromise, serverPromise } = Meteor.callAsync('greetUser', 'John');
+const { stubPromise, serverPromise } = Meteor.callAsync("greetUser", "John");
await stubPromise;
// 🔵 Client simulation
-Greetings.findOne({ name: 'John' }); // đź§ľ Data is available (Optimistic-UI)
+Greetings.findOne({ name: "John" }); // đź§ľ Data is available (Optimistic-UI)
try {
await serverPromise;
// 🟢 Server ended with success
-} catch(e) {
+} catch (e) {
console.error("Error:", error.reason); // đź”´ Server ended with error
}
-Greetings.findOne({ name: 'John' }); // 🗑️ Data is NOT available
+Greetings.findOne({ name: "John" }); // 🗑️ Data is NOT available
```
#### Meteor 2.x contrast
For those familiar with legacy Meteor 2.x, the handling of client simulation and server response was managed using fibers, as explained in the following section. This comparison illustrates how async inclusion with standard promises has transformed the way Meteor operates in modern versions.
-``` javascript
-Meteor.call('greetUser', 'John', function(error, result) {
+```javascript
+Meteor.call("greetUser", "John", function (error, result) {
if (error) {
console.error("Error:", error.reason); // đź”´ Server ended with error
} else {
console.log("Result:", result); // 🟢 Server ended with success
}
- Greetings.findOne({ name: 'John' }); // 🗑️ Data is NOT available
+ Greetings.findOne({ name: "John" }); // 🗑️ Data is NOT available
});
// 🔵 Client simulation
-Greetings.findOne({ name: 'John' }); // đź§ľ Data is available (Optimistic-UI)
+Greetings.findOne({ name: "John" }); // đź§ľ Data is available (Optimistic-UI)
```
-
`Meteor.apply` is just like `Meteor.call`, except that the method arguments are
@@ -504,8 +499,6 @@ different collections. We hope to lift this restriction in a future release.
-
-
```js
import { Meteor } from "meteor/meteor";
import { check } from "meteor/check";
diff --git a/v3-docs/docs/cli/index.md b/v3-docs/docs/cli/index.md
index c72bded0bc..00bdfa2916 100644
--- a/v3-docs/docs/cli/index.md
+++ b/v3-docs/docs/cli/index.md
@@ -53,7 +53,8 @@ This is the default command. Simply running `meteor` is the same as `meteor run`
| `--mobile-server ` | Location where mobile builds connect (defaults to local IP and port). Can include URL scheme (e.g., https://example.com:443) |
| `--cordova-server-port ` | Local port where Cordova will serve content |
| `--production` | Simulate production mode. Minify and bundle CSS and JS files |
-| `--raw-logs` | Run without parsing logs from stdout and stderr |
+| `--raw-logs` | Run without parsing logs from stdout and stderr (default: true) |
+| `--timestamps` | Run with timestamps in logs, the same as passing `--raw-logs=false`. |
| `--settings`, `-s ` | Set optional data for Meteor.settings on the server |
| `--release ` | Specify the release of Meteor to use |
| `--verbose` | Print all output from builds logs |
@@ -247,10 +248,10 @@ If you run `meteor create` without arguments, Meteor will launch an interactive
Typescript # To create an app using TypeScript and React
Vue # To create a basic Vue3-based app
Svelte # To create a basic Svelte app
- Tailwind # To create an app using React and Tailwind
- Chakra-ui # To create an app Chakra UI and React
- Solid # To create a basic Solid app
- Apollo # To create a basic Apollo + React app
+ Tailwind # To create an app using React and Tailwind
+ Chakra-ui # To create an app Chakra UI and React
+ Solid # To create a basic Solid app
+ Apollo # To create a basic Apollo + React app
Bare # To create an empty app
```
:::
@@ -359,7 +360,7 @@ To learn more about the recommended file structure for Meteor apps, check the [M
## meteor generate {meteorgenerate}
-``meteor generate`` is a command to generate boilerplate for your current project. `meteor generate` receives a name as a parameter, and generates files containing code to create a [Collection](https://docs.meteor.com/api/collections.html) with that name, [Methods](https://docs.meteor.com/api/meteor.html#methods) to perform basic CRUD operations on that Collection, and a [Subscription](https://docs.meteor.com/api/meteor.html#Meteor-publish) to read its data with reactivity from the client.
+``meteor generate`` is a command to generate boilerplate for your current project. `meteor generate` receives a name as a parameter, and generates files containing code to create a [Collection](https://docs.meteor.com/api/collections.html) with that name, [Methods](https://docs.meteor.com/api/meteor.html#methods) to perform basic CRUD operations on that Collection, and a [Subscription](https://docs.meteor.com/api/meteor.html#Meteor-publish) to read its data with reactivity from the client.
If you run ``meteor generate`` without arguments, it will ask you for a name, and name the auto-generated Collection accordingly. It will also ask if you do want Methods for your API and Publications to be generated as well.
diff --git a/v3-docs/docs/components/helpers/ParamTable.vue b/v3-docs/docs/components/helpers/ParamTable.vue
index c3d1cdf9ed..8251d3a7a1 100644
--- a/v3-docs/docs/components/helpers/ParamTable.vue
+++ b/v3-docs/docs/components/helpers/ParamTable.vue
@@ -44,6 +44,9 @@ const sourceCode = `https://github.com/meteor/meteor/blob/devel/packages/${props
+
+
+
diff --git a/v3-docs/docs/tutorials/blaze/1.creating-the-app.md b/v3-docs/docs/tutorials/blaze/1.creating-the-app.md
new file mode 100644
index 0000000000..5434274dbd
--- /dev/null
+++ b/v3-docs/docs/tutorials/blaze/1.creating-the-app.md
@@ -0,0 +1,190 @@
+## 1: Creating the app
+
+### 1.1: Install Meteor
+
+First, we need to install Meteor by following this [installation guide](https://docs.meteor.com/about/install.html).
+
+### 1.2: Create Meteor Project
+
+The easiest way to setup Meteor with Blaze is by using the command `meteor create` with the option `--blaze` and your project name:
+
+```shell
+meteor create --blaze simple-todos-blaze
+```
+
+Meteor will create all the necessary files for you.
+
+The files located in the `client` directory are setting up your client side (web), you can see for example `client/main.html` where Meteor is rendering your App main component into the HTML.
+
+Also, check the `server` directory where Meteor is setting up the server side (Node.js), you can see the `server/main.js` which would be a good place to initialize your MongoDB database with some data. You don't need to install MongoDB as Meteor provides an embedded version of it ready for you to use.
+
+You can now run your Meteor app using:
+
+```shell
+meteor
+```
+
+Don't worry, Meteor will keep your app in sync with all your changes from now on.
+
+Take a quick look at all the files created by Meteor, you don't need to understand them now but it's good to know where they are.
+
+Your Blaze code will be located inside the `imports/ui` directory, and the `App.html` and `App.js` files will be the root component of your Blaze To-do app. We haven't made those yet but will soon.
+
+### 1.3: Create Task Component
+
+To start working on our todo list app, let’s replace the code of the default starter app with the code below. From there, we’ll talk about what it does.
+
+First, let’s remove the body from our HTML entry-point (leaving just the `` tag):
+
+::: code-group
+
+```html [client/main.html]
+
+ Simple todo
+
+```
+:::
+
+Create a new directory named `imports` inside the `simple-todos-blaze` folder. In the `imports` folder, create another directory with the name `ui` and add an `App.html` file inside of it with the content below:
+
+::: code-group
+
+```html [imports/ui/App.html]
+
+ {{> mainContainer }}
+
+
+
+
+
+
Todo List
+
+
+
+ {{#each tasks}}
+ {{> task}}
+ {{/each}}
+
+
+
+
+
+
{{text}}
+
+```
+:::
+
+We just created two templates, the `mainContainer`, which will be rendered in the body of our app, and it will show a header and a list of tasks that will render each item using the `task` template. Now, we need some data to present sample tasks on this page.
+
+### 1.4: Create Sample Tasks
+
+Create a new file called `App.js` in your `ui` folder.
+
+Inside your entry-point `main.js` file, remove all the previous content and just add the code below to import the new file `imports/ui/App.js`:
+
+::: code-group
+
+```js [client/main.js]
+import '../imports/ui/App.js';
+```
+:::
+
+As you are not connecting to your server and database yet, let’s define some sample data, which we will use shortly to render a list of tasks. Add the code below to the `App.js` file:
+
+::: code-group
+
+```js [imports/ui/App.js]
+import { Template } from 'meteor/templating';
+
+import './App.html';
+
+Template.mainContainer.helpers({
+ tasks: [
+ { text: 'This is task 1' },
+ { text: 'This is task 2' },
+ { text: 'This is task 3' },
+ ],
+});
+```
+:::
+
+Adding a helper to the `mainContainer` template, you are able to define the array of tasks. When the app starts, the client-side entry-point will import the `App.js` file, which will also import the `App.html` template we created in the previous step.
+
+At this point meteor should be running on port 3000 so you can visit the running app and see your list with three tasks displayed at [http://localhost:3000/](http://localhost:3000/) - but if meteor is not running, go to your terminal and move to the top directory of your project and type `meteor` then press return to launch the app.
+
+All right! Let’s find out what all these bits of code are doing!
+
+### 1.5: Rendering Data
+
+
+
+Meteor parses HTML files and identifies three top-level tags: ``, ``, and ``.
+
+Everything inside any `` tags is added to the head section of the HTML sent to the client, and everything inside `` tags is added to the body section, just like in a regular HTML file.
+
+Everything inside `` tags is compiled into Meteor templates, which can be included inside HTML with {{> templateName}} or referenced in your JavaScript with `Template.templateName`.
+
+Also, the `body` section can be referenced in your JavaScript with `Template.body`. Think of it as a special “parent” template, that can include the other child templates.
+
+All of the code in your HTML files will be compiled with [Meteor’s Spacebars compiler](http://blazejs.org/api/spacebars.html). Spacebars uses statements surrounded by double curly braces such as {{#each}} and {{#if}} to let you add logic and data to your views.
+
+You can pass data into templates from your JavaScript code by defining helpers. In the code above, we defined a helper called `tasks` on `Template.mainContainer` that returns an array. Inside the template tag of the HTML, we can use {{#each tasks}} to iterate over the array and insert a task template for each value. Inside the #each block, we can display the text property of each array item using {{text}}.
+
+### 1.6: Mobile Look
+
+Let’s see how your app is looking on mobile. You can simulate a mobile environment by `right clicking` your app in the browser (we are assuming you are using Google Chrome, as it is the most popular browser) and then `inspect`, this will open a new window inside your browser called `Dev Tools`. In the `Dev Tools` you have a small icon showing a Mobile device and a Tablet:
+
+
+
+Click on it and then select the phone that you want to simulate and in the top nav bar.
+
+> You can also check your app in your personal cellphone. To do so, connect to your App using your local IP in the navigation browser of your mobile browser.
+>
+> This command should print your local IP for you on Unix systems
+> `ifconfig | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}'`
+>
+> On Microsoft Windows try this in a command prompt
+> `ipconfig | findstr "IPv4 Address"`
+
+You should see the following:
+
+
+
+As you can see, everything is small, as we are not adjusting the view port for mobile devices. You can fix this and other similar issues by adding these lines to your `client/main.html` file, inside the `head` tag, after the `title`.
+
+::: code-group
+
+```html [client/main.html]
+...
+
+
+
+
+
+...
+```
+:::
+
+Now your app should look like this:
+
+
+
+
+### 1.7: Hot Module Replacement
+
+By default, when using Blaze with Meteor, a package called [hot-module-replacement](https://docs.meteor.com/packages/hot-module-replacement) is already added for you. This package updates the javascript modules in a running app that were modified during a rebuild. Reduces the feedback cycle while developing, so you can view and test changes quicker (it even updates the app before the build has finished). You are also not going to lose the state, your app code will be updated, and your state will be the same.
+
+> You can read more about packages [here](https://docs.meteor.com/packages/).
+
+You should also add the package [dev-error-overlay](https://atmospherejs.com/meteor/dev-error-overlay) at this point, so you can see the errors in your web browser.
+
+```shell
+meteor add dev-error-overlay
+```
+
+You can try to make some mistakes and then you are going to see the errors in the browser and not only in the console.
+
+In the next step we are going to work with our MongoDB database to be able to store our tasks.
diff --git a/v3-docs/docs/tutorials/blaze/2.collections.md b/v3-docs/docs/tutorials/blaze/2.collections.md
new file mode 100644
index 0000000000..c605f2fdf4
--- /dev/null
+++ b/v3-docs/docs/tutorials/blaze/2.collections.md
@@ -0,0 +1,156 @@
+## 2: Collections
+
+Meteor already sets up MongoDB for you. In order to use our database, we need to create a _collection_, which is where we will store our _documents_, in our case our `tasks`.
+
+> You can read more about collections [here](https://v3-docs.meteor.com/api/collections.html).
+
+In this step we will implement all the necessary code to have a basic collection for our tasks up and running.
+
+### 2.1: Create Tasks Collection {#create-tasks-collection}
+
+Create a new directory in `imports/api` if it doesn't exist already. We can create a new collection to store our tasks by creating a new file at `imports/api/TasksCollection.js` which instantiates a new Mongo collection and exports it.
+
+::: code-group
+
+```js [imports/api/TasksCollection.js]
+import { Mongo } from "meteor/mongo";
+
+export const TasksCollection = new Mongo.Collection("tasks");
+```
+:::
+
+Notice that we stored the file in the `imports/api` directory, which is a place to store API-related code, like publications and methods. You can name this folder as you want, this is just a choice.
+
+> You can read more about app structure and imports/exports [here](http://guide.meteor.com/structure.html).
+
+### 2.2: Initialize Tasks Collection {#initialize-tasks-collection}
+
+For our collection to work, you need to import it in the server so it sets some plumbing up.
+
+You can either use `import "/imports/api/TasksCollection"` or `import { TasksCollection } from "/imports/api/TasksCollection"` if you are going to use on the same file, but make sure it is imported.
+
+Now it is easy to check if there is data or not in our collection, otherwise, we can insert some sample data easily as well.
+
+You don't need to keep the old content of `server/main.js`.
+
+::: code-group
+
+```js [server/main.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "/imports/api/TasksCollection";
+
+const insertTask = (taskText) =>
+ TasksCollection.insertAsync({ text: taskText });
+
+Meteor.startup(async () => {
+ if ((await TasksCollection.find().countAsync()) === 0) {
+ [
+ "First Task",
+ "Second Task",
+ "Third Task",
+ "Fourth Task",
+ "Fifth Task",
+ "Sixth Task",
+ "Seventh Task",
+ ].forEach(insertTask);
+ }
+});
+```
+:::
+
+So you are importing the `TasksCollection` and adding a few tasks to it iterating over an array of strings and for each string calling a function to insert this string as our `text` field in our `task` document.
+
+### 2.3: Render Tasks Collection {#render-tasks-collection}
+
+Now comes the fun part, you will render the tasks with Blaze. That will be pretty simple.
+
+In your `App.js` file, import the `TasksCollection` file and, instead of returning a static array, return the tasks saved in the database:
+
+::: code-group
+
+```javascript [imports/ui/App.js]
+import { Template } from 'meteor/templating';
+import { TasksCollection } from "../api/TasksCollection";
+import './App.html';
+
+Template.mainContainer.helpers({
+ tasks() {
+ return TasksCollection.find({});
+ },
+});
+```
+:::
+
+But wait! Something is missing. If you run your app now, you'll see that you don't render any tasks.
+
+That's because we need to publish our data to the client.
+
+> For more information on Publications/Subscriptions, please check our [docs](https://v3-docs.meteor.com/api/meteor.html#pubsub).
+
+Meteor doesn't need REST calls. It instead relies on synchronizing the MongoDB on the server with a MiniMongoDB on the client. It does this by first publishing collections on the server and then subscribing to them on the client.
+
+First, create a publication for our tasks:
+
+::: code-group
+
+```javascript [imports/api/TasksPublications.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "./TasksCollection";
+
+Meteor.publish("tasks", function () {
+ return TasksCollection.find();
+});
+```
+:::
+
+Now, we need to import this file in our server:
+
+::: code-group
+
+```js [server/main.js]
+...
+import { TasksCollection } from '/imports/api/TasksCollection';
+
+import "../imports/api/TasksPublications"; // [!code highlight]
+
+const insertTask = taskText => TasksCollection.insertAsync({ text: taskText });
+...
+```
+:::
+
+The only thing left is subscribe to this publication:
+
+::: code-group
+
+```javascript [imports/ui/App.js]
+...
+
+Template.mainContainer.onCreated(function mainContainerOnCreated() {
+ Meteor.subscribe('tasks');
+});
+
+...
+```
+:::
+
+See how your app should look like now:
+
+
+
+You can change your data on MongoDB in the server and your app will react and re-render for you.
+
+You can connect to your MongoDB running `meteor mongo` in the terminal from your app folder or using a Mongo UI client, like [NoSQLBooster](https://nosqlbooster.com/downloads). Your embedded MongoDB is running in port `3001`.
+
+See how to connect:
+
+
+
+See your database:
+
+
+
+You can double-click your collection to see the documents stored on it:
+
+
+
+In the next step, we are going to create tasks using a form.
diff --git a/v3-docs/docs/tutorials/blaze/3.forms-and-events.md b/v3-docs/docs/tutorials/blaze/3.forms-and-events.md
new file mode 100644
index 0000000000..053aafe0c3
--- /dev/null
+++ b/v3-docs/docs/tutorials/blaze/3.forms-and-events.md
@@ -0,0 +1,205 @@
+## 3: Forms and Events
+
+All apps need to allow the user to perform some sort of interaction with the data that is stored. In our case, the first type of interaction is to insert new tasks. Without it, our To-Do app wouldn't be very helpful.
+
+One of the main ways in which a user can insert or edit data on a website is through forms. In most cases, it is a good idea to use the `
+ );
+};
+```
+:::
+
+### 4.2: Toggle Checkbox
+
+Now you can update your task document by toggling its `isChecked` field.
+
+First, create a new method called `tasks.toggleChecked` to update the `isChecked` property.
+
+::: code-group
+
+```js [imports/api/TasksMethods.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "./TasksCollection";
+
+Meteor.methods({
+ ...
+ "tasks.toggleChecked"({ _id, isChecked }) {
+ return TasksCollection.updateAsync(_id, {
+ $set: { isChecked: !isChecked },
+ });
+ },
+});
+```
+:::
+
+In `Task.jsx` (as shown above), we've added an `onChange` handler that calls the method to toggle the checked state.
+
+Toggling checkboxes should now persist in the DB even if you refresh the web browser.
+
+Your app should look like this:
+
+
+
+If your computer is fast enough, it's possible that when it sets up the default tasks a few will have the same date. That will cause them to non-deterministically "jump around" in the UI as you toggle checkboxes and the UI reactively updates. To make it stable, you can add a secondary sort on the `_id` of the task:
+
+::: code-group
+
+```jsx [imports/ui/App.jsx]
+...
+
+Tracker.autorun(async () => {
+ setIsReady(subscription.ready());
+ setTasks(await TasksCollection.find({}, { sort: { createdAt: -1, _id: -1 } }).fetchAsync()); // [!code highlight]
+});
+
+...
+```
+:::
+
+
+### 4.3: Remove tasks
+
+You can remove tasks with just a few lines of code.
+
+First, add a button after the `label` in your `Task` component and a `deleteTask` function.
+
+::: code-group
+
+```jsx [imports/ui/Task.jsx]
+import { Meteor } from "meteor/meteor";
+
+export const Task = (props) => {
+ const { task } = props;
+
+ const toggleChecked = async () => {
+ await Meteor.callAsync("tasks.toggleChecked", { _id: task._id, isChecked: task.isChecked });
+ };
+
+ const deleteTask = async () => { // [!code highlight]
+ await Meteor.callAsync("tasks.delete", { _id: task._id }); // [!code highlight]
+ }; // [!code highlight]
+
+ return (
+
+
+
+ // [!code highlight]
+
+ );
+};
+```
+:::
+
+Next you need to have a function to delete the task. For that, let's create a new method called `tasks.delete`:
+
+::: code-group
+
+```js [imports/api/TasksMethods.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "./TasksCollection";
+
+Meteor.methods({
+ ...
+ "tasks.delete"({ _id }) {
+ return TasksCollection.removeAsync(_id);
+ },
+});
+```
+:::
+
+Now the removal logic is handled in `Task.jsx` via the `onClick` event on the delete button, which calls the Meteor method.
+
+Your app should look like this:
+
+
+
+### 4.4: Getting data in event handlers
+
+In a collection, every inserted document has a unique `_id` field that can refer to that specific document. Inside the component, the `task` prop provides access to the task object, including its `_id`
+and other fields like `isChecked` and `text`. We use these to call Meteor methods for updating or removing the specific task.
+
+In the next step, we are going to improve the look of your app using CSS with Flexbox.
+
diff --git a/v3-docs/docs/tutorials/solid/5.styles.md b/v3-docs/docs/tutorials/solid/5.styles.md
new file mode 100644
index 0000000000..d8e4542f11
--- /dev/null
+++ b/v3-docs/docs/tutorials/solid/5.styles.md
@@ -0,0 +1,231 @@
+## 5: Styles
+
+### 5.1: CSS
+
+Our user interface up until this point has looked quite ugly. Let's add some basic styling which will serve as the foundation for a more professional looking app.
+
+Replace the content of our `imports/ui/main.css` file with the one below, the idea is to have an app bar at the top, and a scrollable content including:
+
+- form to add new tasks;
+- list of tasks.
+
+::: code-group
+
+```css [imports/ui/main.css]
+body {
+ font-family: sans-serif;
+ background-color: #315481;
+ background-image: linear-gradient(to bottom, #315481, #918e82 100%);
+ background-attachment: fixed;
+
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+
+ padding: 0;
+ margin: 0;
+
+ font-size: 14px;
+}
+
+button {
+ font-weight: bold;
+ font-size: 1em;
+ border: none;
+ color: white;
+ box-shadow: 0 3px 3px rgba(34, 25, 25, 0.4);
+ padding: 5px;
+ cursor: pointer;
+}
+
+button:focus {
+ outline: 0;
+}
+
+.app {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+.app-header {
+ flex-grow: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.main {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ overflow: auto;
+ background: white;
+}
+
+.main::-webkit-scrollbar {
+ width: 0;
+ height: 0;
+ background: inherit;
+}
+
+header {
+ background: #d2edf4;
+ background-image: linear-gradient(to bottom, #d0edf5, #e1e5f0 100%);
+ padding: 20px 15px 15px 15px;
+ position: relative;
+ box-shadow: 0 3px 3px rgba(34, 25, 25, 0.4);
+}
+
+.app-bar {
+ display: flex;
+ justify-content: space-between;
+}
+
+.app-bar h1 {
+ font-size: 1.5em;
+ margin: 0;
+ display: inline-block;
+ margin-right: 1em;
+}
+
+.task-form {
+ display: flex;
+ margin: 16px;
+}
+
+.task-form > input {
+ flex-grow: 1;
+ box-sizing: border-box;
+ padding: 10px 6px;
+ background: transparent;
+ border: 1px solid #aaa;
+ width: 100%;
+ font-size: 1em;
+ margin-right: 16px;
+}
+
+.task-form > input:focus {
+ outline: 0;
+}
+
+.task-form > button {
+ min-width: 100px;
+ height: 95%;
+ background-color: #315481;
+}
+
+.tasks {
+ list-style-type: none;
+ padding-inline-start: 0;
+ padding-left: 16px;
+ padding-right: 16px;
+ margin-block-start: 0;
+ margin-block-end: 0;
+}
+
+.tasks > li {
+ display: flex;
+ padding: 16px;
+ border-bottom: #eee solid 1px;
+ align-items: center;
+}
+
+.tasks > li > label {
+ flex-grow: 1;
+}
+
+.tasks > li > button {
+ justify-self: flex-end;
+ background-color: #ff3046;
+}
+```
+:::
+
+> If you want to learn more about this stylesheet check this article about [Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/), and also this free [video tutorial](https://flexbox.io/) about it from [Wes Bos](https://twitter.com/wesbos).
+>
+> Flexbox is an excellent tool to distribute and align elements in your UI.
+
+### 5.2: Applying styles
+
+Now you need to add some elements around your components. You are going to add a `class` to your main `div` in the `App.jsx`, also a `header` element with a few `div` elements around your `h1`, and a main `div` around your form and list. Check below how it should be; pay attention to the name of the classes, they need to be the same as in the CSS file:
+
+::: code-group
+
+```jsx [imports/ui/App.jsx]
+import { createSignal, For, Show } from "solid-js";
+import { Meteor } from "meteor/meteor";
+import { Tracker } from "meteor/tracker";
+import { TasksCollection } from "../api/TasksCollection";
+import { Task } from "./Task.jsx";
+
+export const App = () => {
+ const [newTask, setNewTask] = createSignal('');
+
+ const addTask = async (event) => {
+ event.preventDefault();
+ if (newTask().trim()) {
+ await Meteor.callAsync("tasks.insert", {
+ text: newTask(),
+ createdAt: new Date(),
+ });
+ setNewTask('');
+ }
+ };
+
+ const subscription = Meteor.subscribe("tasks");
+ const [isReady, setIsReady] = createSignal(subscription.ready());
+ const [tasks, setTasks] = createSignal([]);
+
+ Tracker.autorun(async () => {
+ setIsReady(subscription.ready());
+ setTasks(await TasksCollection.find({}, { sort: { createdAt: -1, _id: -1 } }).fetchAsync());
+ });
+
+ return (
+
+
+
+
+
📝️ Todo List
+
+
+
+
+
+
+ setNewTask(e.currentTarget.value)}
+ />
+
+
+
+ Loading ...
}
+ >
+
+
+ {(task) => (
+
+ )}
+
+
+
+
+
+ );
+};
+```
+:::
+
+Your app should look like this:
+
+
+
+In the next step, we are going to make this task list more interactive, for example, providing a way to filter tasks.
diff --git a/v3-docs/docs/tutorials/solid/6.filter-tasks.md b/v3-docs/docs/tutorials/solid/6.filter-tasks.md
new file mode 100644
index 0000000000..75679904e3
--- /dev/null
+++ b/v3-docs/docs/tutorials/solid/6.filter-tasks.md
@@ -0,0 +1,322 @@
+## 6: Filter tasks
+
+In this step, you will filter your tasks by status and show the number of pending tasks.
+
+### 6.1: Reactive State
+
+First, you will add a button to show or hide the completed tasks from the list.
+
+In Solid, we manage component state using signals for reactivity. Solid's fine-grained reactivity will automatically update the UI when the state changes.
+
+We'll add a `hideCompleted` variable to the `App.jsx` component and a function to toggle it.
+
+::: code-group
+
+```jsx [imports/ui/App.jsx]
+import { createSignal, For, Show } from "solid-js";
+import { Meteor } from "meteor/meteor";
+import { Tracker } from "meteor/tracker";
+import { TasksCollection } from "../api/TasksCollection";
+import { Task } from "./Task.jsx";
+
+export const App = () => {
+ const [newTask, setNewTask] = createSignal('');
+ const [hideCompleted, setHideCompleted] = createSignal(false); // [!code highlight]
+
+ const addTask = async (event) => {
+ event.preventDefault();
+ if (newTask().trim()) {
+ await Meteor.callAsync("tasks.insert", {
+ text: newTask(),
+ createdAt: new Date(),
+ });
+ setNewTask('');
+ }
+ };
+
+ const toggleHideCompleted = () => { // [!code highlight]
+ setHideCompleted(!hideCompleted()); // [!code highlight]
+ }; // [!code highlight]
+
+ const subscription = Meteor.subscribe("tasks");
+ const [isReady, setIsReady] = createSignal(subscription.ready());
+ const [tasks, setTasks] = createSignal([]);
+
+ Tracker.autorun(async () => {
+ setIsReady(subscription.ready());
+ setTasks(await TasksCollection.find({}, { sort: { createdAt: -1, _id: -1 } }).fetchAsync());
+ });
+
+ // markup will be updated in next steps
+};
+```
+:::
+
+Then, add the button in the markup to toggle the state:
+
+::: code-group
+
+```jsx [imports/ui/App.jsx]
+// ... javascript above remains the same
+
+return (
+
+
+
+
+
📝️ Todo List
+
+
+
+
+
+
+ setNewTask(e.currentTarget.value)}
+ />
+
+
+
+
// [!code highlight]
+ // [!code highlight]
+
// [!code highlight]
+
+ Loading ...
}
+ >
+
+
+ {(task) => (
+
+ )}
+
+
+
+
+
+);
+```
+:::
+
+You may notice we’re using `` for the button text. You can learn more about Solid's conditional rendering [here](https://docs.solidjs.com/reference/components/show).
+
+### 6.2: Button style
+
+You should add some style to your button so it does not look gray and without a good contrast. You can use the styles below as a reference:
+
+::: code-group
+
+```css [imports/ui/main.css]
+.filter {
+ display: flex;
+ justify-content: center;
+}
+
+.filter > button {
+ background-color: #62807e;
+}
+```
+
+:::
+
+### 6.3: Filter Tasks
+
+Now, update the reactive tasks fetch to apply the filter if `hideCompleted` is true. We'll also add a reactive signal for the incomplete count using another Tracker.autorun.
+
+::: code-group
+
+```jsx [imports/ui/App.jsx]
+import { createSignal, For, Show } from "solid-js";
+import { Meteor } from "meteor/meteor";
+import { Tracker } from "meteor/tracker";
+import { TasksCollection } from "../api/TasksCollection";
+import { Task } from "./Task.jsx";
+
+export const App = () => {
+ const [newTask, setNewTask] = createSignal('');
+ const [hideCompleted, setHideCompleted] = createSignal(false);
+
+ const addTask = async (event) => {
+ event.preventDefault();
+ if (newTask().trim()) {
+ await Meteor.callAsync("tasks.insert", {
+ text: newTask(),
+ createdAt: new Date(),
+ });
+ setNewTask('');
+ }
+ };
+
+ const toggleHideCompleted = () => {
+ setHideCompleted(!hideCompleted());
+ };
+
+ const subscription = Meteor.subscribe("tasks");
+ const [isReady, setIsReady] = createSignal(subscription.ready());
+ const [tasks, setTasks] = createSignal([]);
+
+ Tracker.autorun(async () => {
+ setIsReady(subscription.ready());
+ const query = hideCompleted() ? { isChecked: { $ne: true } } : {}; // [!code highlight]
+ setTasks(await TasksCollection.find(query, { sort: { createdAt: -1, _id: -1 } }).fetchAsync()); // [!code highlight]
+ });
+
+ // Reactive incomplete count // [!code highlight]
+ const [incompleteCount, setIncompleteCount] = createSignal(0); // [!code highlight]
+ Tracker.autorun(async () => { // [!code highlight]
+ setIncompleteCount(await TasksCollection.find({ isChecked: { $ne: true } }).countAsync()); // [!code highlight]
+ }); // [!code highlight]
+
+ // markup remains the same
+};
+```
+
+:::
+
+### 6.4: Meteor Dev Tools Extension
+
+You can install an extension to visualize the data in your Mini Mongo.
+
+[Meteor DevTools Evolved](https://chrome.google.com/webstore/detail/meteor-devtools-evolved/ibniinmoafhgbifjojidlagmggecmpgf) will help you to debug your app as you can see what data is on Mini Mongo.
+
+
+
+You can also see all the messages that Meteor is sending and receiving from the server, this is useful for you to learn more about how Meteor works.
+
+
+
+Install it in your Google Chrome browser using this [link](https://chrome.google.com/webstore/detail/meteor-devtools-evolved/ibniinmoafhgbifjojidlagmggecmpgf).
+
+### 6.5: Pending tasks
+
+Update the App component in order to show the number of pending tasks in the app bar.
+
+You should avoid adding zero to your app bar when there are no pending tasks. Use the reactive `incompleteCount` in the header:
+
+::: code-group
+
+```jsx [imports/ui/App.jsx]
+// ... javascript with incompleteCount remains the same
+
+return (
+
+
+
+
+
📝️ To Do List {incompleteCount() > 0 ? `(${incompleteCount()})` : ''}
// [!code highlight]
+
+
+
+
+ // rest of markup
+
+);
+```
+
+:::
+
+### 6.6: Make the Hide/Show toggle work
+
+If you try the Hide/Show completed toggle button you'll see that the text changes but it doesn't actually do anything to the list of tasks. The toggle button isn't triggering the expected filtering because the `hideCompleted` signal (from Solid) isn't integrated with Meteor's Tracker reactivity system. Tracker.autorun only re-runs when its internal dependencies (like subscription data or ReactiveVars) change—not when Solid signals change. So, updating `hideCompleted` via `setHideCompleted` changes the button text (via Solid's ``), but it doesn't re-execute the autorun to update the query and re-fetch tasks.
+
+This is a common challenge when bridging Solid's fine-grained reactivity with Meteor's Tracker. I'll explain the fix below with minimal changes to your code.
+
+To make Tracker react to changes in `hideCompleted`, we'll use Meteor's `ReactiveVar`
+to hold the filter state. This makes the value reactive within Tracker, so the autorun re-runs automatically when it changes. We'll sync it with your Solid signal for the UI.
+
+Add this import at the top of `App.jsx`
+::: code-group
+
+```jsx [imports/ui/App.jsx]
+import { ReactiveVar } from 'meteor/reactive-var';
+```
+
+:::
+
+Initialize a ReactiveVar and use a Solid `createEffect` to keep it in sync with your `hideCompleted` signal. Update your `App` component like this:
+
+Add this import at the top of `App.jsx`
+::: code-group
+
+```jsx [imports/ui/App.jsx]
+...
+import { createSignal, For, Show, createEffect } from "solid-js"; // Updated import
+...
+
+export const App = () => {
+ const [newTask, setNewTask] = createSignal('');
+ const [hideCompleted, setHideCompleted] = createSignal(false);
+
+ // New: ReactiveVar for Tracker integration
+ const hideCompletedVar = new ReactiveVar(false); // [!code highlight]
+
+ // New: Sync Solid signal to ReactiveVar (triggers Tracker re-run) // [!code highlight]
+ createEffect(() => { // [!code highlight]
+ hideCompletedVar.set(hideCompleted()); // [!code highlight]
+ }); // [!code highlight]
+
+ const addTask = async (event) => {
+ event.preventDefault();
+ if (newTask().trim()) {
+ await Meteor.callAsync("tasks.insert", {
+ text: newTask(),
+ createdAt: new Date(),
+ });
+ setNewTask('');
+ }
+ };
+
+ const toggleHideCompleted = () => {
+ setHideCompleted(!hideCompleted());
+ };
+
+ const subscription = Meteor.subscribe("tasks");
+ const [isReady, setIsReady] = createSignal(subscription.ready());
+ const [tasks, setTasks] = createSignal([]);
+
+ Tracker.autorun(async () => {
+ setIsReady(subscription.ready());
+ // Use ReactiveVar in the query for Tracker reactivity // [!code highlight]
+ const query = hideCompletedVar.get() ? { isChecked: { $ne: true } } : {}; // [!code highlight]
+ setTasks(await TasksCollection.find(query, { sort: { createdAt: -1, _id: -1 } }).fetchAsync());
+ });
+
+ // Reactive incomplete count (unchanged)
+ const [incompleteCount, setIncompleteCount] = createSignal(0);
+ Tracker.autorun(async () => {
+ setIncompleteCount(await TasksCollection.find({ isChecked: { $ne: true } }).countAsync());
+ });
+
+ // Return statement remains the same
+ return (
+ // ... your JSX here, unchanged
+ );
+};
+```
+
+:::
+
+Why this works:
+
+1. `ReactiveVar` bridges the systems: When you toggle `hideCompleted` (Solid signal), the `createEffect` updates the `ReactiveVar`. This invalidates and re-runs the Tracker.autorun, which re-fetches tasks with the updated query.
+2. No major rewrites: Your existing signals and UI logic stay intact.
+3. Performance: Tracker handles the re-fetch efficiently, and Solid updates the UI via the `tasks` signal.
+
+
+Your app should look like this:
+
+
+
+
+In the next step we are going to include user access in your app.
diff --git a/v3-docs/docs/tutorials/solid/7.adding-user-accounts.md b/v3-docs/docs/tutorials/solid/7.adding-user-accounts.md
new file mode 100644
index 0000000000..23d78a2682
--- /dev/null
+++ b/v3-docs/docs/tutorials/solid/7.adding-user-accounts.md
@@ -0,0 +1,414 @@
+## 7: Adding User Accounts
+
+### 7.1: Password Authentication
+
+Meteor already comes with a basic authentication and account management system out of the box, so you only need to add the `accounts-password` to enable username and password authentication:
+
+```shell
+meteor add accounts-password
+```
+
+> There are many more authentication methods supported. You can read more about the accounts system [here](https://v3-docs.meteor.com/api/accounts.html).
+
+We also recommend you to install `bcrypt` node module, otherwise, you are going to see a warning saying that you are using a pure-Javascript implementation of it.
+
+```shell
+meteor npm install --save bcrypt
+```
+
+> You should always use `meteor npm` instead of only `npm` so you always use the `npm` version pinned by Meteor, this helps you to avoid problems due to different versions of npm installing different modules.
+
+### 7.2: Create User Account
+
+Now you can create a default user for our app, we are going to use `meteorite` as username, we just create a new user on server startup if we didn't find it in the database.
+
+::: code-group
+
+```js [server/main.js]
+import { Meteor } from 'meteor/meteor';
+import { Accounts } from 'meteor/accounts-base'; // [!code highlight]
+import { TasksCollection } from '/imports/api/TasksCollection';
+import "../imports/api/TasksPublications";
+import "../imports/api/TasksMethods";
+
+const SEED_USERNAME = 'meteorite'; // [!code highlight]
+const SEED_PASSWORD = 'password'; // [!code highlight]
+
+Meteor.startup(async () => {
+ if (!(await Accounts.findUserByUsername(SEED_USERNAME))) { // [!code highlight]
+ await Accounts.createUser({ // [!code highlight]
+ username: SEED_USERNAME, // [!code highlight]
+ password: SEED_PASSWORD, // [!code highlight]
+ }); // [!code highlight]
+ } // [!code highlight]
+
+ ...
+});
+```
+:::
+
+You should not see anything different in your app UI yet.
+
+### 7.3: Login Form
+
+You need to provide a way for the users to input the credentials and authenticate, for that we need a form.
+
+Our login form will be simple, with just two fields (username and password) and a button. You should use `Meteor.loginWithPassword(username, password)`; to authenticate your user with the provided inputs.
+
+Create a new component `Login.jsx` in `imports/ui/`:
+
+::: code-group
+
+```jsx [imports/ui/Login.jsx]
+import { createSignal } from "solid-js";
+import { Meteor } from "meteor/meteor";
+
+export const Login = () => {
+ const [username, setUsername] = createSignal('');
+ const [password, setPassword] = createSignal('');
+
+ const login = async (event) => {
+ event.preventDefault();
+ await Meteor.loginWithPassword(username(), password());
+ };
+
+ return (
+
+
+
+ setUsername(e.currentTarget.value)}
+ />
+
+
+
+
+ setPassword(e.currentTarget.value)}
+ />
+
+
+
+
+
+ );
+};
+```
+:::
+
+Be sure also to import the login form in `App.jsx`.
+
+::: code-group
+
+```jsx [imports/ui/App.jsx]
+import { ReactiveVar } from 'meteor/reactive-var';
+import { createSignal, For, Show, createEffect } from "solid-js";
+import { Meteor } from "meteor/meteor";
+import { Tracker } from "meteor/tracker";
+import { TasksCollection } from "../api/TasksCollection";
+import { Task } from "./Task.jsx";
+import { Login } from "./Login.jsx"; // [!code highlight]
+
+// ... rest of the script
+```
+:::
+
+Ok, now you have a form, let's use it.
+
+### 7.4: Require Authentication
+
+Our app should only allow an authenticated user to access its task management features.
+
+We can accomplish that by rendering the `Login` component when we don’t have an authenticated user. Otherwise, we render the form, filter, and list.
+
+To achieve this, we will use a conditional `` in `App.jsx`:
+
+::: code-group
+
+```jsx [imports/ui/App.jsx]
+// ... other imports and code
+
+const [currentUser, setCurrentUser] = createSignal(Meteor.user()); // Reactive current user // [!code highlight]
+Tracker.autorun(() => { // [!code highlight]
+ setCurrentUser(Meteor.user()); // [!code highlight]
+}); // [!code highlight]
+
+// ... rest of script
+
+return (
+
+
+
+
+
📝️ To Do List {incompleteCount() > 0 ? `(${incompleteCount()})` : ''}
+
+);
+```
+:::
+
+As you can see, if the user is logged in, we render the whole app (`currentUser` is truthy). Otherwise, we render the Login component.
+
+### 7.5: Login Form style
+
+Ok, let's style the login form now:
+
+::: code-group
+
+```css [imports/ui/main.css]
+.login-form {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ justify-content: center;
+ align-items: center;
+}
+
+.login-form > div {
+ margin: 8px;
+}
+
+.login-form > div > label {
+ font-weight: bold;
+}
+
+.login-form > div > input {
+ flex-grow: 1;
+ box-sizing: border-box;
+ padding: 10px 6px;
+ background: transparent;
+ border: 1px solid #aaa;
+ width: 100%;
+ font-size: 1em;
+ margin-right: 16px;
+ margin-top: 4px;
+}
+
+.login-form > div > input:focus {
+ outline: 0;
+}
+
+.login-form > div > button {
+ background-color: #62807e;
+}
+```
+:::
+
+Now your login form should be centralized and beautiful.
+
+### 7.6: Server startup
+
+Every task should have an owner from now on. So go to your database, as you learned before, and remove all the tasks from there:
+
+`db.tasks.remove({});`
+
+Change your `server/main.js` to add the seed tasks using your `meteorite` user as owner.
+
+Make sure you restart the server after this change so `Meteor.startup` block will run again. This is probably going to happen automatically anyway as you are going to make changes in the server side code.
+
+::: code-group
+
+```js [server/main.js]
+...
+
+ const user = await Accounts.findUserByUsername(SEED_USERNAME);
+
+ if ((await TasksCollection.find().countAsync()) === 0) {
+ [
+ "First Task",
+ "Second Task",
+ "Third Task",
+ "Fourth Task",
+ "Fifth Task",
+ "Sixth Task",
+ "Seventh Task",
+ ].forEach((taskName) => {
+ Meteor.callAsync("tasks.insert", {
+ text: taskName,
+ createdAt: new Date(),
+ userId: user._id
+ });
+ });
+ }
+
+...
+```
+:::
+
+See that we are using a new field called `userId` with our user `_id` field, we are also setting `createdAt` field.
+
+### 7.7: Task owner
+
+First, let's change our publication to publish the tasks only for the currently logged user. This is important for security, as you send only data that belongs to that user.
+
+::: code-group
+
+```js [/imports/api/TasksPublications.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "./TasksCollection";
+
+Meteor.publish("tasks", function () {
+ let result = this.ready();
+ const userId = this.userId;
+ if (userId) {
+ result = TasksCollection.find({ userId });
+ }
+
+ return result;
+});
+```
+:::
+
+Now let's check if we have a `currentUser` before trying to fetch any data. Update the reactive `tasks` and `incompleteCount` to only run if logged in:
+
+::: code-group
+
+```jsx [imports/ui/App.jsx]
+// ... other imports and code
+
+ Tracker.autorun(async () => {
+ setIsReady(subscription.ready());
+ if (!Meteor.userId()) { // [!code highlight] // Skip if not logged in
+ setTasks([]); // [!code highlight]
+ return; // [!code highlight]
+ } // [!code highlight]
+ // Use ReactiveVar in the query for Tracker reactivity
+ const query = hideCompletedVar.get() ? { isChecked: { $ne: true } } : {};
+ setTasks(await TasksCollection.find(query, { sort: { createdAt: -1, _id: -1 } }).fetchAsync());
+ });
+
+ // Reactive incomplete count
+ const [incompleteCount, setIncompleteCount] = createSignal(0);
+ Tracker.autorun(async () => {
+ if (!Meteor.userId()) { // [!code highlight] // Skip if not logged in
+ setIncompleteCount(0); // [!code highlight]
+ return; // [!code highlight]
+ } // [!code highlight]
+ setIncompleteCount(await TasksCollection.find({ isChecked: { $ne: true } }).countAsync());
+ });
+
+// ... rest of component
+```
+:::
+
+Also, update the `tasks.insert` method to include the field `userId` when creating a new task:
+
+::: code-group
+
+```js [imports/api/TasksMethods.js]
+...
+Meteor.methods({
+ "tasks.insert"(doc) {
+ const insertDoc = { ...doc };
+ if (!('userId' in insertDoc)) {
+ insertDoc.userId = this.userId;
+ }
+ return TasksCollection.insertAsync(insertDoc);
+ },
+...
+```
+
+:::
+
+### 7.8: Log out
+
+We can also organize our tasks by showing the owner’s username below our app bar. Let’s add a new `div` where the user can click and log out from the app:
+
+::: code-group
+
+```jsx [imports/ui/App.jsx]
+...
+
+
// [!code highlight]
+
+ ...
+```
+:::
+
+Remember to style your username as well.
+
+::: code-group
+
+```css [imports/ui/main.css]
+.user {
+ display: flex;
+
+ align-self: flex-end;
+
+ margin: 8px 16px 0;
+ font-weight: bold;
+ cursor: pointer;
+}
+```
+
+:::
+
+Phew! You have done quite a lot in this step. Authenticated the user, set the user in the tasks, and provided a way for the user to log out.
+
+Your app should look like this:
+
+
+
+
+In the next step, we are going to learn how to deploy your app!
diff --git a/v3-docs/docs/tutorials/solid/8.deploying.md b/v3-docs/docs/tutorials/solid/8.deploying.md
new file mode 100644
index 0000000000..589ab8d062
--- /dev/null
+++ b/v3-docs/docs/tutorials/solid/8.deploying.md
@@ -0,0 +1,98 @@
+## 8: Deploying
+
+Deploying a Meteor application is similar to deploying any other Node.js app that uses websockets. You can find deployment options in [our guide](https://guide.meteor.com/deployment), including Meteor Up, Docker, and our recommended method, Galaxy.
+
+In this tutorial, we will deploy our app on [Galaxy](https://galaxycloud.app/), which is our own cloud solution. Galaxy offers a free plan, so you can deploy and test your app. Pretty cool, right?
+
+
+### 8.1: Create your account
+
+
+You need a Meteor account to deploy your apps. If you don’t have one yet, you can [sign up here](https://cloud.meteor.com/?isSignUp=true).
+With this account, you can access our package manager, [Atmosphere](https://atmospherejs.com/), [Forums](https://forums.meteor.com/) and more.
+
+
+
+### 8.2: Set up MongoDB (Optional)
+
+
+As your app uses MongoDB the first step is to set up a MongoDB database, Galaxy offers MongoDB hosting on a free plan for testing purposes, and you can also request for a production ready database that allows you to scale.
+
+In any MongoDB provider you will have a MongoDB URL which you must use. If you use the free option provided by Galaxy, the initial setup is done for you.
+
+Galaxy MongoDB URL will be like this: `mongodb://username:@org-dbname-01.mongodb.galaxy-cloud.io` .
+> You can read more about Galaxy MongoDB [here](https://galaxy-support.meteor.com/en/article/mongodb-general-1syd5af/).
+
+### 8.3: Set up settings
+
+
+If you are not using the free option, then you need to create a settings file. It’s a JSON file that Meteor apps can read configurations from. Create this file in a new folder called `private` in the root of your project. It is important to notice that `private` is a special folder that is not going to be published to the client side of your app.
+
+Make sure you replace `Your MongoDB URL` by your own MongoDB URL :)
+
+
+::: code-group
+```json [private/settings.json]
+{
+ "galaxy.meteor.com": {
+ "env": {
+ "MONGO_URL": "Your MongoDB URL"
+ }
+ }
+}
+```
+:::
+
+### 8.4: Deploy it
+
+
+Now you are ready to deploy, run `meteor npm install` before deploying to make sure all your dependencies are installed.
+
+You also need to choose a subdomain to publish your app. We are going to use the main domain `meteorapp.com` that is free and included on any Galaxy plan.
+
+In this example we are going to use `solid-meteor-3.meteorapp.com` but make sure you select a different one, otherwise you are going to receive an error.
+
+
+> You can learn how to use custom domains on Galaxy [here](https://galaxy-support.meteor.com/en/article/domains-16cijgc/). Custom domains are available starting with the Essentials plan.
+
+Run the deployment command:
+
+```shell
+meteor deploy solid-meteor-3.meteorapp.com --free --mongo
+```
+
+> If you are not using the free hosting with MongoDB on Galaxy, then remove the `--mongo` flag from the deploy script and add `--settings private/settings.json` with the proper setting for your app.
+
+Make sure you replace `solid-meteor-3` by a custom name that you want as subdomain. You will see a log like this:
+
+```shell
+meteor deploy solid-meteor-3.meteorapp.com --settings private/settings.json
+Talking to Galaxy servers at https://us-east-1.galaxy-deploy.meteor.com
+Preparing to build your app...
+Preparing to upload your app...
+Uploaded app bundle for new app at solid-meteor-3.meteorapp.com.
+Galaxy is building the app into a native image.
+Waiting for deployment updates from Galaxy...
+Building app image...
+Deploying app...
+You have successfully deployed the first version of your app.
+For details, visit https://galaxy.meteor.com/app/solid-meteor-3.meteorapp.com
+```
+
+
+This process usually takes just a few minutes, but it depends on your internet speed as it’s going to send your app bundle to Galaxy servers.
+
+> Galaxy builds a new Docker image that contains your app bundle and then deploy containers using it, [read more](https://galaxy-support.meteor.com/en/article/container-environment-lfd6kh/).
+You can check your logs on Galaxy, including the part that Galaxy is building your Docker image and deploying it.
+
+### 8.5: Access the app and enjoy
+
+
+Now you should be able to access your Galaxy dashboard at `https://galaxy.meteor.com/app/solid-meteor-3.meteorapp.com`.
+
+You can also access your app on Galaxy 2.0 which is currently in beta at `https://galaxy-beta.meteor.com//us-east-1/apps/.meteorapp.com`. Remember to use your own subdomain instead of `solid-meteor-3`.
+
+You can access the app at [solid-meteor-3.meteorapp.com](https://solid-meteor-3.meteorapp.com/)! Just use your subdomain to access yours!
+
+> We deployed to Galaxy running in the US (us-east-1), we also have Galaxy running in other regions in the world, check the list [here](https://galaxy-support.meteor.com/en/article/regions-1vucejm/).
+This is huge, you have your app running on Galaxy, ready to be used by anyone in the world!
diff --git a/v3-docs/docs/tutorials/solid/9.next-steps.md b/v3-docs/docs/tutorials/solid/9.next-steps.md
new file mode 100644
index 0000000000..2d8aa59038
--- /dev/null
+++ b/v3-docs/docs/tutorials/solid/9.next-steps.md
@@ -0,0 +1,17 @@
+## 9: Next Steps
+
+You have completed the tutorial!
+
+By now, you should have a good understanding of working with Meteor and Solid.
+
+::: info
+You can find the final version of this app in our [GitHub repository](https://github.com/meteor/meteor3-solid).
+:::
+
+Here are some options for what you can do next:
+
+- Check out the complete [documentation](https://v3-docs.meteor.com/) to learn more about Meteor 3.
+- Read the [Galaxy Guide](https://galaxy-support.meteor.com/en/article/deploy-to-galaxy-18gd6e2/) to learn more about deploying your app.
+- Join our community on the [Meteor Forums](https://forums.meteor.com/) and the [Meteor Lounge on Discord](https://discord.gg/hZkTCaVjmT) to ask questions and share your experiences.
+
+We can't wait to see what you build next!
diff --git a/v3-docs/docs/tutorials/solid/assets/collections-connect-db.png b/v3-docs/docs/tutorials/solid/assets/collections-connect-db.png
new file mode 100644
index 0000000000..6e29b8eb70
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/collections-connect-db.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/collections-documents.png b/v3-docs/docs/tutorials/solid/assets/collections-documents.png
new file mode 100644
index 0000000000..3b7e0a2a99
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/collections-documents.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/collections-see-database.png b/v3-docs/docs/tutorials/solid/assets/collections-see-database.png
new file mode 100644
index 0000000000..3a3115fe80
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/collections-see-database.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/collections-tasks-list.png b/v3-docs/docs/tutorials/solid/assets/collections-tasks-list.png
new file mode 100644
index 0000000000..cc7924ca4f
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/collections-tasks-list.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/mermaid-diagram-solid-rendering.png b/v3-docs/docs/tutorials/solid/assets/mermaid-diagram-solid-rendering.png
new file mode 100644
index 0000000000..4ff3be89cb
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/mermaid-diagram-solid-rendering.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/mermaid-diagram-solid-rendering.svg b/v3-docs/docs/tutorials/solid/assets/mermaid-diagram-solid-rendering.svg
new file mode 100644
index 0000000000..36880fe8d0
--- /dev/null
+++ b/v3-docs/docs/tutorials/solid/assets/mermaid-diagram-solid-rendering.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/v3-docs/docs/tutorials/solid/assets/step01-dev-tools-mobile-toggle.png b/v3-docs/docs/tutorials/solid/assets/step01-dev-tools-mobile-toggle.png
new file mode 100644
index 0000000000..4865e38742
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step01-dev-tools-mobile-toggle.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/step01-mobile-with-meta-tags.png b/v3-docs/docs/tutorials/solid/assets/step01-mobile-with-meta-tags.png
new file mode 100644
index 0000000000..f864cc1815
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step01-mobile-with-meta-tags.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/step01-mobile-without-meta-tags.png b/v3-docs/docs/tutorials/solid/assets/step01-mobile-without-meta-tags.png
new file mode 100644
index 0000000000..e23043ea91
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step01-mobile-without-meta-tags.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/step03-form-new-task.png b/v3-docs/docs/tutorials/solid/assets/step03-form-new-task.png
new file mode 100644
index 0000000000..44cf04444c
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step03-form-new-task.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/step03-new-task-on-list.png b/v3-docs/docs/tutorials/solid/assets/step03-new-task-on-list.png
new file mode 100644
index 0000000000..971d7a4c4c
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step03-new-task-on-list.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/step04-checkbox.png b/v3-docs/docs/tutorials/solid/assets/step04-checkbox.png
new file mode 100644
index 0000000000..930425ac2d
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step04-checkbox.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/step04-delete-button.png b/v3-docs/docs/tutorials/solid/assets/step04-delete-button.png
new file mode 100644
index 0000000000..29a8be7afa
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step04-delete-button.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/step05-styles.png b/v3-docs/docs/tutorials/solid/assets/step05-styles.png
new file mode 100644
index 0000000000..790abbe8fe
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step05-styles.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/step06-all.png b/v3-docs/docs/tutorials/solid/assets/step06-all.png
new file mode 100644
index 0000000000..e0934440f6
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step06-all.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/step06-ddp-messages.png b/v3-docs/docs/tutorials/solid/assets/step06-ddp-messages.png
new file mode 100644
index 0000000000..6d54b038f4
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step06-ddp-messages.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/step06-extension.png b/v3-docs/docs/tutorials/solid/assets/step06-extension.png
new file mode 100644
index 0000000000..0f7400a8be
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step06-extension.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/step06-filtered.png b/v3-docs/docs/tutorials/solid/assets/step06-filtered.png
new file mode 100644
index 0000000000..8830409760
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step06-filtered.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/step07-login.png b/v3-docs/docs/tutorials/solid/assets/step07-login.png
new file mode 100644
index 0000000000..6437e20ed8
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step07-login.png differ
diff --git a/v3-docs/docs/tutorials/solid/assets/step07-logout.png b/v3-docs/docs/tutorials/solid/assets/step07-logout.png
new file mode 100644
index 0000000000..a63d60d728
Binary files /dev/null and b/v3-docs/docs/tutorials/solid/assets/step07-logout.png differ
diff --git a/v3-docs/docs/tutorials/solid/index.md b/v3-docs/docs/tutorials/solid/index.md
new file mode 100644
index 0000000000..0ea7dd52a5
--- /dev/null
+++ b/v3-docs/docs/tutorials/solid/index.md
@@ -0,0 +1,25 @@
+# Meteor.js 3 + Solid
+
+In this tutorial, we will create a simple To-Do app using [Solid](https://www.solidjs.com/) and Meteor 3.0. Meteor works well with other frameworks like [React](https://react.dev/), [Vue 3](https://vuejs.org/), [Svelte](https://svelte.dev/), and [Blaze](https://www.blazejs.org/).
+
+Solid is a modern UI framework that compiles your reactive code to highly efficient DOM updates at runtime, resulting in smaller bundles and exceptional performance without a virtual DOM. Launched in 2020, it has gained popularity for its fine-grained reactivity, simplicity, and lightweight nature. Compared to older approaches, Solid eliminates much of the boilerplate and runtime overhead found in frameworks like React by using a compiler that optimizes updates precisely where needed. It employs a declarative JSX syntax with built-in primitives like signals for state management, effects, and resources that can be seamlessly integrated with Meteor's reactive data sources like[Tracker](https://docs.meteor.com/api/tracker.html) and [Minimongo](https://docs.meteor.com/api/collections.html). This means your UI updates automatically as data changes, without manual DOM manipulation.
+
+If you're new and not sure what UI framework to use, Solid is a great place to start—it's easy to learn (especially if you're familiar with React-like JSX), highly performant with fine-grained reactivity, and has a growing community. You can still leverage Meteor packages designed for other frameworks, like [accounts-ui](https://docs.meteor.com/packages/accounts-ui), even in a Solid app.
+
+To start building your Solid app, you'll need a code editor. If you're unsure which one to choose, [Visual Studio Code](https://code.visualstudio.com/) is a good option.
+
+Let’s begin building your app!
+
+# Table of Contents
+
+[[toc]]
+
+
+
+
+
+
+
+
+
+
diff --git a/v3-docs/docs/tutorials/svelte/1.creating-the-app.md b/v3-docs/docs/tutorials/svelte/1.creating-the-app.md
new file mode 100644
index 0000000000..eaed84ca41
--- /dev/null
+++ b/v3-docs/docs/tutorials/svelte/1.creating-the-app.md
@@ -0,0 +1,183 @@
+## 1: Creating the app
+
+### 1.1: Install Meteor
+
+First, we need to install Meteor by following this [installation guide](https://docs.meteor.com/about/install.html).
+
+### 1.2: Create Meteor Project
+
+The easiest way to setup Meteor with Svelte is by using the command `meteor create` with the option `--svelte` and your project name:
+
+```shell
+meteor create --svelte simple-todos-svelte
+```
+
+Meteor will create all the necessary files for you.
+
+The files located in the `client` directory are setting up your client side (web), you can see for example `client/main.html` where Meteor is rendering your App main component into the HTML.
+
+Also, check the `server` directory where Meteor is setting up the server side (Node.js), you can see the `server/main.js` which would be a good place to initialize your MongoDB database with some data. You don't need to install MongoDB as Meteor provides an embedded version of it ready for you to use.
+
+You can now run your Meteor app using:
+
+```shell
+meteor
+```
+
+Don't worry, Meteor will keep your app in sync with all your changes from now on.
+
+Take a quick look at all the files created by Meteor, you don't need to understand them now but it's good to know where they are.
+
+Your Svelte code will be located inside the `imports/ui` directory, and the `App.svelte` file will be the root component of your Svelte To-do app.
+
+### 1.3: Create Tasks
+
+To start working on our todo list app, let’s replace the code of the default starter app with the code below. From there, we’ll talk about what it does.
+
+First, let’s simplify our HTML entry point like so:
+
+::: code-group
+
+```html [client/main.html]
+
+ Simple todo
+
+
+
+
+
+```
+:::
+
+The `client/main.js` file should import and render the main Svelte component:
+
+::: code-group
+
+```js [client/main.js]
+import { Meteor } from 'meteor/meteor';
+import App from '../imports/ui/App.svelte';
+
+Meteor.startup(() => {
+ new App({
+ target: document.getElementById('app')
+ });
+});
+```
+:::
+
+
+Inside the `imports/ui` folder let us modify `App.svelte` to display a header and a list of tasks:
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+
+
+
+
+
Todo List
+
+
+
+ {#each tasks as task (task.text)}
+
{task.text}
+ {/each}
+
+
+```
+:::
+
+We just modified our main Svelte component `App.svelte`, which will be rendered into the `#app` div in the body. It shows a header and a list of tasks. For now, we're using static sample data to display the tasks.
+
+### 1.4: View Sample Tasks
+
+As you are not connecting to your server and database yet, we’ve defined some sample data directly in `App.svelte` to render a list of tasks.
+
+At this point meteor should be running on port 3000 so you can visit the running app and see your list with three tasks displayed at [http://localhost:3000/](http://localhost:3000/) - but if meteor is not running, go to your terminal and move to the top directory of your project and type `meteor` then press return to launch the app.
+
+All right! Let’s find out what all these bits of code are doing!
+
+### 1.5: Rendering Data
+
+
+
+In Svelte with Meteor, your main entry point is `client/main.js`, which imports and mounts your root Svelte component `App.svelte` to a target element in the HTML like ``
+
+Svelte components are defined in `.svelte` files, which can include `
+
+
+
+
Todo List
+
+
+
+ {#if subIsReady}
+ {#each tasks as task (task._id)}
+
{task.text}
+ {/each}
+ {:else}
+
Loading ...
+ {/if}
+
+
+```
+:::
+
+But wait! Something is missing. If you run your app now, you'll see that you don't render any tasks.
+
+That's because we need to publish our data to the client.
+
+> For more information on Publications/Subscriptions, please check our [docs](https://v3-docs.meteor.com/api/meteor.html#pubsub).
+
+Meteor doesn't need REST calls. It instead relies on synchronizing the MongoDB on the server with a MiniMongoDB on the client. It does this by first publishing collections on the server and then subscribing to them on the client.
+
+First, create a publication for our tasks:
+
+::: code-group
+
+```javascript [imports/api/TasksPublications.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "./TasksCollection";
+
+Meteor.publish("tasks", function () {
+ return TasksCollection.find();
+});
+```
+:::
+
+Now, we need to import this file in our server:
+
+::: code-group
+
+```js [server/main.js]
+...
+import { TasksCollection } from '/imports/api/TasksCollection';
+
+import "../imports/api/TasksPublications"; // [!code highlight]
+
+const insertTask = taskText => TasksCollection.insertAsync({ text: taskText });
+...
+```
+:::
+
+The only thing left is subscribe to this publication, which we've already added in `App.svelte` using:
+
+::: code-group
+
+```javascript [imports/ui/App.svelte]
+...
+
+$m: handle = Meteor.subscribe("tasks");
+
+...
+```
+:::
+
+See how your app should look like now:
+
+
+
+You can change your data on MongoDB in the server and your app will react and re-render for you.
+
+You can connect to your MongoDB running `meteor mongo` in the terminal from your app folder or using a Mongo UI client, like [NoSQLBooster](https://nosqlbooster.com/downloads). Your embedded MongoDB is running in port `3001`.
+
+See how to connect:
+
+
+
+See your database:
+
+
+
+You can double-click your collection to see the documents stored on it:
+
+
+
+In the next step, we are going to create tasks using a form.
diff --git a/v3-docs/docs/tutorials/svelte/3.forms-and-events.md b/v3-docs/docs/tutorials/svelte/3.forms-and-events.md
new file mode 100644
index 0000000000..bfb570033f
--- /dev/null
+++ b/v3-docs/docs/tutorials/svelte/3.forms-and-events.md
@@ -0,0 +1,221 @@
+## 3: Forms and Events
+
+All apps need to allow the user to perform some sort of interaction with the data that is stored. In our case, the first type of interaction is to insert new tasks. Without it, our To-Do app wouldn't be very helpful.
+
+One of the main ways in which a user can insert or edit data on a website is through forms. In most cases, it is a good idea to use the `
` tag since it gives semantic meaning to the elements inside it.
+
+### 3.1: Create Task Form
+
+Create a new `form` inside the `App.svelte` file, and inside we’ll add an input field and a button. Place it between the `header` and the `ul`:
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+...
+
+
+
+
+
+
+...
+```
+:::
+
+Let's now add an `addTask()` function handler inside the script tags below the imports. It will call a Meteor method `tasks.insert` that isn't yet created but we'll soon make:
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+...
+
+ let newTask = '';
+
+ async function addTask(event) {
+ event.preventDefault();
+ if (newTask.trim()) {
+ await Meteor.callAsync("tasks.insert", {
+ text: newTask,
+ createdAt: new Date(),
+ });
+ newTask = '';
+ }
+ }
+
+...
+```
+:::
+
+Altogether, our file should look like:
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+
+
+
+
+
Todo List
+
+
+
+
+
+
+
+
+ {#if subIsReady}
+ {#each tasks as task (task._id)}
+
{task.text}
+ {/each}
+ {:else}
+
Loading ...
+ {/if}
+
+
+```
+:::
+
+In the code above, we've integrated the form directly into the `App.svelte`
+component, positioning it above the task list. We're using Svelte's `bind:value`
+for two-way binding on the input and an `on:submit` handler for form submission.
+
+### 3.2: Update the Stylesheet
+
+You also can style it as you wish. For now, we only need some margin at the top so the form doesn't seem off the mark. Add the CSS class `.task-form`, this needs to be the same name in your `class` attribute in the form element.
+
+::: code-group
+
+```css [client/main.css]
+...
+
+.task-form {
+ margin-top: 1rem;
+}
+
+...
+```
+:::
+
+### 3.3: Add Submit Handler
+
+Now let's create a function to handle the form submit and insert a new task into the database. To do it, we will need to implement a Meteor Method.
+
+Methods are essentially RPC calls to the server that let you perform operations on the server side securely. You can read more about Meteor Methods [here](https://guide.meteor.com/methods.html).
+
+To create your methods, you can create a file called `TasksMethods.js`.
+
+::: code-group
+
+```javascript [imports/api/TasksMethods.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "./TasksCollection";
+
+Meteor.methods({
+ "tasks.insert"(doc) {
+ return TasksCollection.insertAsync(doc);
+ },
+});
+```
+:::
+
+Remember to import your method on the `main.js` server file, delete the `insertTask` function, and invoke the new meteor method inside the `forEach` block.
+
+::: code-group
+
+```javascript [server/main.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "/imports/api/TasksCollection";
+import "../imports/api/TasksPublications";
+import "../imports/api/TasksMethods"; // [!code highlight]
+
+Meteor.startup(async () => {
+ if ((await TasksCollection.find().countAsync()) === 0) {
+ [
+ "First Task",
+ "Second Task",
+ "Third Task",
+ "Fourth Task",
+ "Fifth Task",
+ "Sixth Task",
+ "Seventh Task",
+ ].forEach((taskName) => {
+ Meteor.callAsync("tasks.insert", { // [!code highlight]
+ text: taskName, // [!code highlight]
+ createdAt: new Date(), // [!code highlight]
+ });
+ });
+ }
+});
+```
+:::
+
+Inside the `forEach` function, we are adding a task to the `tasks` collection by calling `Meteor.callAsync()`. The first argument is the name of the method we want to call, and the second argument is the text of the task.
+
+Also, insert a date `createdAt` in your `task` document so you know when each task was created.
+
+Now we need to import `TasksMethods.js` and add a listener to the `submit` event on the form:
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+
+
+
+```
+:::
+
+In the `addTask` function (shown in 3.1), we prevent the default form submission, get the input value, call the Meteor method to insert the task optimistically, and clear the input.
+
+Meteor methods execute optimistically on the client using MiniMongo while simultaneously calling the server. If the server call fails, MiniMongo rolls back the change, providing a speedy user experience. It's a bit like [rollback netcode](https://glossary.infil.net/?t=Rollback%20Netcode) in fighting video games.
+
+### 3.4: Show Newest Tasks First
+
+Now you just need to make a change that will make users happy: we need to show the newest tasks first. We can accomplish this quite quickly by sorting our [Mongo](https://guide.meteor.com/collections.html#mongo-collections) query.
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+
+
+
+```
+:::
+
+Your app should look like this:
+
+
+
+
+
+In the next step, we are going to update your tasks state and provide a way for users to remove tasks.
diff --git a/v3-docs/docs/tutorials/svelte/4.update-and-remove.md b/v3-docs/docs/tutorials/svelte/4.update-and-remove.md
new file mode 100644
index 0000000000..48d7c3707a
--- /dev/null
+++ b/v3-docs/docs/tutorials/svelte/4.update-and-remove.md
@@ -0,0 +1,197 @@
+## 4: Update and Remove
+
+Up until now, you have only inserted documents into our collection. Let's take a look at how you can update and remove them by interacting with the user interface.
+
+### 4.1: Add Checkbox
+
+First, you need to add a `checkbox` element to your `Task` component.
+
+Next, let’s create a new file for our `task` template in `imports/ui/Task.svelte`, so we can start to separate the logic in our app.
+
+::: code-group
+
+```html [imports/ui/Task.svelte]
+
+
+
+
+
+```
+:::
+
+Now, update `App.svelte` to import and use the `Task` component in the {#each} loop. Remove the old `
+```
+:::
+
+### 4.2: Toggle Checkbox
+
+Now you can update your task document by toggling its `isChecked` field.
+
+First, create a new method called `tasks.toggleChecked` to update the `isChecked` property.
+
+::: code-group
+
+```js [imports/api/TasksMethods.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "./TasksCollection";
+
+Meteor.methods({
+ ...
+ "tasks.toggleChecked"({ _id, isChecked }) {
+ return TasksCollection.updateAsync(_id, {
+ $set: { isChecked: !isChecked },
+ });
+ },
+});
+```
+:::
+
+In `Task.svelte` (as shown above), we've added an `on:change` handler that calls the method to toggle the checked state.
+
+Toggling checkboxes should now persist in the DB even if you refresh the web browser.
+
+Your app should look like this:
+
+
+
+If your computer is fast enough, it's possible that when it sets up the default tasks a few will have the same date. That will cause them to non-deterministically "jump around" in the UI as you toggle checkboxes and the UI reactively updates. To make it stable, you can add a secondary sort on the `_id` of the task:
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+
+
+
+```
+:::
+
+
+### 4.3: Remove tasks
+
+You can remove tasks with just a few lines of code.
+
+First, add a button after the `label` in your `Task` component and a `deleteTask` function between the `script` tags.
+
+::: code-group
+
+```html [imports/ui/Task.svelte]
+
+
+
+
+
+
+
+```
+:::
+
+Next you need to have a function to delete the task. For that, let's create a new method called `tasks.delete`:
+
+::: code-group
+
+```js [imports/api/TasksMethods.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "./TasksCollection";
+
+Meteor.methods({
+ ...
+ "tasks.delete"({ _id }) {
+ return TasksCollection.removeAsync(_id);
+ },
+});
+```
+:::
+
+Now the removal logic is handled in `Task.svelte` via the `on:click` event on the delete button, which calls the Meteor method.
+
+Your app should look like this:
+
+
+
+### 4.4: Getting data in event handlers
+
+In a collection, every inserted document has a unique `_id` field that can refer to that specific document. Inside the component, the `task` prop provides access to the task object, including its `_id`
+and other fields like `isChecked` and `text`. We use these to call Meteor methods for updating or removing the specific task.
+
+In the next step, we are going to improve the look of your app using CSS with Flexbox.
+
diff --git a/v3-docs/docs/tutorials/svelte/5.styles.md b/v3-docs/docs/tutorials/svelte/5.styles.md
new file mode 100644
index 0000000000..bdfb613df4
--- /dev/null
+++ b/v3-docs/docs/tutorials/svelte/5.styles.md
@@ -0,0 +1,215 @@
+## 5: Styles
+
+### 5.1: CSS
+
+Our user interface up until this point has looked quite ugly. Let's add some basic styling which will serve as the foundation for a more professional looking app.
+
+Replace the content of our `client/main.css` file with the one below, the idea is to have an app bar at the top, and a scrollable content including:
+
+- form to add new tasks;
+- list of tasks.
+
+::: code-group
+
+```css [client/main.css]
+body {
+ font-family: sans-serif;
+ background-color: #315481;
+ background-image: linear-gradient(to bottom, #315481, #918e82 100%);
+ background-attachment: fixed;
+
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+
+ padding: 0;
+ margin: 0;
+
+ font-size: 14px;
+}
+
+button {
+ font-weight: bold;
+ font-size: 1em;
+ border: none;
+ color: white;
+ box-shadow: 0 3px 3px rgba(34, 25, 25, 0.4);
+ padding: 5px;
+ cursor: pointer;
+}
+
+button:focus {
+ outline: 0;
+}
+
+.app {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+.app-header {
+ flex-grow: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.main {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ overflow: auto;
+ background: white;
+}
+
+.main::-webkit-scrollbar {
+ width: 0;
+ height: 0;
+ background: inherit;
+}
+
+header {
+ background: #d2edf4;
+ background-image: linear-gradient(to bottom, #d0edf5, #e1e5f0 100%);
+ padding: 20px 15px 15px 15px;
+ position: relative;
+ box-shadow: 0 3px 3px rgba(34, 25, 25, 0.4);
+}
+
+.app-bar {
+ display: flex;
+ justify-content: space-between;
+}
+
+.app-bar h1 {
+ font-size: 1.5em;
+ margin: 0;
+ display: inline-block;
+ margin-right: 1em;
+}
+
+.task-form {
+ display: flex;
+ margin: 16px;
+}
+
+.task-form > input {
+ flex-grow: 1;
+ box-sizing: border-box;
+ padding: 10px 6px;
+ background: transparent;
+ border: 1px solid #aaa;
+ width: 100%;
+ font-size: 1em;
+ margin-right: 16px;
+}
+
+.task-form > input:focus {
+ outline: 0;
+}
+
+.task-form > button {
+ min-width: 100px;
+ height: 95%;
+ background-color: #315481;
+}
+
+.tasks {
+ list-style-type: none;
+ padding-inline-start: 0;
+ padding-left: 16px;
+ padding-right: 16px;
+ margin-block-start: 0;
+ margin-block-end: 0;
+}
+
+.tasks > li {
+ display: flex;
+ padding: 16px;
+ border-bottom: #eee solid 1px;
+ align-items: center;
+}
+
+.tasks > li > label {
+ flex-grow: 1;
+}
+
+.tasks > li > button {
+ justify-self: flex-end;
+ background-color: #ff3046;
+}
+```
+:::
+
+> If you want to learn more about this stylesheet check this article about [Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/), and also this free [video tutorial](https://flexbox.io/) about it from [Wes Bos](https://twitter.com/wesbos).
+>
+> Flexbox is an excellent tool to distribute and align elements in your UI.
+
+### 5.2: Applying styles
+
+Now you need to add some elements around your components. You are going to add a `class` to your main `div` in the `App.svelte`, also a `header` element with a few `div` elements around your `h1`, and a main `div` around your form and list. Check below how it should be; pay attention to the name of the classes, they need to be the same as in the CSS file:
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+
+
+
+```
+:::
+
+Your app should look like this:
+
+
+
+In the next step, we are going to make this task list more interactive, for example, providing a way to filter tasks.
diff --git a/v3-docs/docs/tutorials/svelte/6.filter-tasks.md b/v3-docs/docs/tutorials/svelte/6.filter-tasks.md
new file mode 100644
index 0000000000..82035aca7d
--- /dev/null
+++ b/v3-docs/docs/tutorials/svelte/6.filter-tasks.md
@@ -0,0 +1,211 @@
+## 6: Filter tasks
+
+In this step, you will filter your tasks by status and show the number of pending tasks.
+
+### 6.1: Reactive State
+
+First, you will add a button to show or hide the completed tasks from the list.
+
+In Svelte, we can manage component state using reactive variables directly in the `
+
+
+```
+:::
+
+Then, add the button in the markup to toggle the state:
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+
+
+
+```
+:::
+
+You may notice we’re using {#if} (a conditional block) for the button text. You can learn more about Svelte's conditional rendering [here](https://svelte.dev/docs#client-side-component-api).
+
+### 6.2: Button style
+
+You should add some style to your button so it does not look gray and without a good contrast. You can use the styles below as a reference:
+
+::: code-group
+
+```css [client/main.css]
+.filter {
+ display: flex;
+ justify-content: center;
+}
+
+.filter > button {
+ background-color: #62807e;
+}
+```
+
+:::
+
+### 6.3: Filter Tasks
+
+Now, update the reactive tasks fetch to apply the filter if `hideCompleted` is true. We'll also add a reactive variable for the incomplete count.
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+
+
+
+```
+
+:::
+
+### 6.4: Meteor Dev Tools Extension
+
+You can install an extension to visualize the data in your Mini Mongo.
+
+[Meteor DevTools Evolved](https://chrome.google.com/webstore/detail/meteor-devtools-evolved/ibniinmoafhgbifjojidlagmggecmpgf) will help you to debug your app as you can see what data is on Mini Mongo.
+
+
+
+You can also see all the messages that Meteor is sending and receiving from the server, this is useful for you to learn more about how Meteor works.
+
+
+
+Install it in your Google Chrome browser using this [link](https://chrome.google.com/webstore/detail/meteor-devtools-evolved/ibniinmoafhgbifjojidlagmggecmpgf).
+
+### 6.5: Pending tasks
+
+Update the App component in order to show the number of pending tasks in the app bar.
+
+You should avoid adding zero to your app bar when there are no pending tasks. Use the reactive `incompleteDisplay` in the header:
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+
+
+
+
+
+
+
📝️ To Do List {incompleteDisplay}
+
+
+
+
+
+
+```
+
+:::
+
+Your app should look like this:
+
+
+
+
+In the next step we are going to include user access in your app.
diff --git a/v3-docs/docs/tutorials/svelte/7.adding-user-accounts.md b/v3-docs/docs/tutorials/svelte/7.adding-user-accounts.md
new file mode 100644
index 0000000000..b6318a85d1
--- /dev/null
+++ b/v3-docs/docs/tutorials/svelte/7.adding-user-accounts.md
@@ -0,0 +1,390 @@
+## 7: Adding User Accounts
+
+### 7.1: Password Authentication
+
+Meteor already comes with a basic authentication and account management system out of the box, so you only need to add the `accounts-password` to enable username and password authentication:
+
+```shell
+meteor add accounts-password
+```
+
+> There are many more authentication methods supported. You can read more about the accounts system [here](https://v3-docs.meteor.com/api/accounts.html).
+
+We also recommend you to install `bcrypt` node module, otherwise, you are going to see a warning saying that you are using a pure-Javascript implementation of it.
+
+```shell
+meteor npm install --save bcrypt
+```
+
+> You should always use `meteor npm` instead of only `npm` so you always use the `npm` version pinned by Meteor, this helps you to avoid problems due to different versions of npm installing different modules.
+
+### 7.2: Create User Account
+
+Now you can create a default user for our app, we are going to use `meteorite` as username, we just create a new user on server startup if we didn't find it in the database.
+
+::: code-group
+
+```js [server/main.js]
+import { Meteor } from 'meteor/meteor';
+import { Accounts } from 'meteor/accounts-base'; // [!code highlight]
+import { TasksCollection } from '/imports/api/TasksCollection';
+import "../imports/api/TasksPublications";
+import "../imports/api/TasksMethods";
+
+const SEED_USERNAME = 'meteorite'; // [!code highlight]
+const SEED_PASSWORD = 'password'; // [!code highlight]
+
+Meteor.startup(async () => {
+ if (!(await Accounts.findUserByUsername(SEED_USERNAME))) { // [!code highlight]
+ await Accounts.createUser({ // [!code highlight]
+ username: SEED_USERNAME, // [!code highlight]
+ password: SEED_PASSWORD, // [!code highlight]
+ }); // [!code highlight]
+ } // [!code highlight]
+
+ ...
+});
+```
+:::
+
+You should not see anything different in your app UI yet.
+
+### 7.3: Login Form
+
+You need to provide a way for the users to input the credentials and authenticate, for that we need a form.
+
+Our login form will be simple, with just two fields (username and password) and a button. You should use `Meteor.loginWithPassword(username, password)`; to authenticate your user with the provided inputs.
+
+Create a new component `Login.svelte` in `imports/ui/`:
+
+::: code-group
+
+```html [imports/ui/Login.svelte]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+:::
+
+Be sure also to import the login form in `App.svelte`.
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+
+
+
+```
+:::
+
+Ok, now you have a form, let's use it.
+
+### 7.4: Require Authentication
+
+Our app should only allow an authenticated user to access its task management features.
+
+We can accomplish that by rendering the `Login` component when we don’t have an authenticated user. Otherwise, we render the form, filter, and list.
+
+To achieve this, we will use a conditional block in `App.svelte`:
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+
+
+
+```
+:::
+
+As you can see, if the user is logged in, we render the whole app (`currentUser` is truthy). Otherwise, we render the Login component.
+
+### 7.5: Login Form style
+
+Ok, let's style the login form now:
+
+::: code-group
+
+```css [client/main.css]
+.login-form {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ justify-content: center;
+ align-items: center;
+}
+
+.login-form > div {
+ margin: 8px;
+}
+
+.login-form > div > label {
+ font-weight: bold;
+}
+
+.login-form > div > input {
+ flex-grow: 1;
+ box-sizing: border-box;
+ padding: 10px 6px;
+ background: transparent;
+ border: 1px solid #aaa;
+ width: 100%;
+ font-size: 1em;
+ margin-right: 16px;
+ margin-top: 4px;
+}
+
+.login-form > div > input:focus {
+ outline: 0;
+}
+
+.login-form > div > button {
+ background-color: #62807e;
+}
+```
+:::
+
+Now your login form should be centralized and beautiful.
+
+### 7.6: Server startup
+
+Every task should have an owner from now on. So go to your database, as you learned before, and remove all the tasks from there:
+
+`db.tasks.remove({});`
+
+Change your `server/main.js` to add the seed tasks using your `meteorite` user as owner.
+
+Make sure you restart the server after this change so `Meteor.startup` block will run again. This is probably going to happen automatically anyway as you are going to make changes in the server side code.
+
+::: code-group
+
+```js [server/main.js]
+...
+
+ const user = await Accounts.findUserByUsername(SEED_USERNAME);
+
+ if ((await TasksCollection.find().countAsync()) === 0) {
+ [
+ "First Task",
+ "Second Task",
+ "Third Task",
+ "Fourth Task",
+ "Fifth Task",
+ "Sixth Task",
+ "Seventh Task",
+ ].forEach((taskName) => {
+ Meteor.callAsync("tasks.insert", {
+ text: taskName,
+ createdAt: new Date(),
+ userId: user._id
+ });
+ });
+ }
+
+...
+```
+:::
+
+See that we are using a new field called `userId` with our user `_id` field, we are also setting `createdAt` field.
+
+### 7.7: Task owner
+
+First, let's change our publication to publish the tasks only for the currently logged user. This is important for security, as you send only data that belongs to that user.
+
+::: code-group
+
+```js [/imports/api/TasksPublications.js]
+import { Meteor } from "meteor/meteor";
+import { TasksCollection } from "./TasksCollection";
+
+Meteor.publish("tasks", function () {
+ let result = this.ready();
+ const userId = this.userId;
+ if (userId) {
+ result = TasksCollection.find({ userId });
+ }
+
+ return result;
+});
+```
+:::
+
+Now let's check if we have a `currentUser` before trying to fetch any data. Update the reactive `tasks` and `incompleteCount` to only run if logged in:
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+
+
+
+```
+:::
+
+Also, update the `tasks.insert` method to include the field `userId` when creating a new task:
+
+::: code-group
+
+```js [imports/api/TasksMethods.js]
+...
+Meteor.methods({
+ "tasks.insert"(doc) {
+ const insertDoc = { ...doc };
+ if (!('userId' in insertDoc)) {
+ insertDoc.userId = this.userId;
+ }
+ return TasksCollection.insertAsync(insertDoc);
+ },
+...
+```
+
+:::
+
+### 7.8: Log out
+
+We can also organize our tasks by showing the owner’s username below our app bar. Let’s add a new `div` where the user can click and log out from the app:
+
+::: code-group
+
+```html [imports/ui/App.svelte]
+...
+
+ {#if currentUser}
+
Meteor.logout()}>
+ {currentUser.username} 🚪
+
+
+ ...
+```
+:::
+
+Remember to style your username as well.
+
+::: code-group
+
+```css [client/main.css]
+.user {
+ display: flex;
+
+ align-self: flex-end;
+
+ margin: 8px 16px 0;
+ font-weight: bold;
+ cursor: pointer;
+}
+```
+
+:::
+
+Phew! You have done quite a lot in this step. Authenticated the user, set the user in the tasks, and provided a way for the user to log out.
+
+Your app should look like this:
+
+
+
+
+In the next step, we are going to learn how to deploy your app!
diff --git a/v3-docs/docs/tutorials/svelte/8.deploying.md b/v3-docs/docs/tutorials/svelte/8.deploying.md
new file mode 100644
index 0000000000..352f8f7300
--- /dev/null
+++ b/v3-docs/docs/tutorials/svelte/8.deploying.md
@@ -0,0 +1,98 @@
+## 8: Deploying
+
+Deploying a Meteor application is similar to deploying any other Node.js app that uses websockets. You can find deployment options in [our guide](https://guide.meteor.com/deployment), including Meteor Up, Docker, and our recommended method, Galaxy.
+
+In this tutorial, we will deploy our app on [Galaxy](https://galaxycloud.app/), which is our own cloud solution. Galaxy offers a free plan, so you can deploy and test your app. Pretty cool, right?
+
+
+### 8.1: Create your account
+
+
+You need a Meteor account to deploy your apps. If you don’t have one yet, you can [sign up here](https://cloud.meteor.com/?isSignUp=true).
+With this account, you can access our package manager, [Atmosphere](https://atmospherejs.com/), [Forums](https://forums.meteor.com/) and more.
+
+
+
+### 8.2: Set up MongoDB (Optional)
+
+
+As your app uses MongoDB the first step is to set up a MongoDB database, Galaxy offers MongoDB hosting on a free plan for testing purposes, and you can also request for a production ready database that allows you to scale.
+
+In any MongoDB provider you will have a MongoDB URL which you must use. If you use the free option provided by Galaxy, the initial setup is done for you.
+
+Galaxy MongoDB URL will be like this: `mongodb://username:@org-dbname-01.mongodb.galaxy-cloud.io` .
+> You can read more about Galaxy MongoDB [here](https://galaxy-support.meteor.com/en/article/mongodb-general-1syd5af/).
+
+### 8.3: Set up settings
+
+
+If you are not using the free option, then you need to create a settings file. It’s a JSON file that Meteor apps can read configurations from. Create this file in a new folder called `private` in the root of your project. It is important to notice that `private` is a special folder that is not going to be published to the client side of your app.
+
+Make sure you replace `Your MongoDB URL` by your own MongoDB URL :)
+
+
+::: code-group
+```json [private/settings.json]
+{
+ "galaxy.meteor.com": {
+ "env": {
+ "MONGO_URL": "Your MongoDB URL"
+ }
+ }
+}
+```
+:::
+
+### 8.4: Deploy it
+
+
+Now you are ready to deploy, run `meteor npm install` before deploying to make sure all your dependencies are installed.
+
+You also need to choose a subdomain to publish your app. We are going to use the main domain `meteorapp.com` that is free and included on any Galaxy plan.
+
+In this example we are going to use `svelte-meteor-3.meteorapp.com` but make sure you select a different one, otherwise you are going to receive an error.
+
+
+> You can learn how to use custom domains on Galaxy [here](https://galaxy-support.meteor.com/en/article/domains-16cijgc/). Custom domains are available starting with the Essentials plan.
+
+Run the deployment command:
+
+```shell
+meteor deploy svelte-meteor-3.meteorapp.com --free --mongo
+```
+
+> If you are not using the free hosting with MongoDB on Galaxy, then remove the `--mongo` flag from the deploy script and add `--settings private/settings.json` with the proper setting for your app.
+
+Make sure you replace `svelte-meteor-3` by a custom name that you want as subdomain. You will see a log like this:
+
+```shell
+meteor deploy svelte-meteor-3.meteorapp.com --settings private/settings.json
+Talking to Galaxy servers at https://us-east-1.galaxy-deploy.meteor.com
+Preparing to build your app...
+Preparing to upload your app...
+Uploaded app bundle for new app at svelte-meteor-3.meteorapp.com.
+Galaxy is building the app into a native image.
+Waiting for deployment updates from Galaxy...
+Building app image...
+Deploying app...
+You have successfully deployed the first version of your app.
+For details, visit https://galaxy.meteor.com/app/svelte-meteor-3.meteorapp.com
+```
+
+
+This process usually takes just a few minutes, but it depends on your internet speed as it’s going to send your app bundle to Galaxy servers.
+
+> Galaxy builds a new Docker image that contains your app bundle and then deploy containers using it, [read more](https://galaxy-support.meteor.com/en/article/container-environment-lfd6kh/).
+You can check your logs on Galaxy, including the part that Galaxy is building your Docker image and deploying it.
+
+### 8.5: Access the app and enjoy
+
+
+Now you should be able to access your Galaxy dashboard at `https://galaxy.meteor.com/app/svelte-meteor-3.meteorapp.com`.
+
+You can also access your app on Galaxy 2.0 which is currently in beta at `https://galaxy-beta.meteor.com//us-east-1/apps/.meteorapp.com`. Remember to use your own subdomain instead of `svelte-meteor-3`.
+
+You can access the app at [svelte-meteor-3.meteorapp.com](https://svelte-meteor-3.meteorapp.com/)! Just use your subdomain to access yours!
+
+> We deployed to Galaxy running in the US (us-east-1), we also have Galaxy running in other regions in the world, check the list [here](https://galaxy-support.meteor.com/en/article/regions-1vucejm/).
+This is huge, you have your app running on Galaxy, ready to be used by anyone in the world!
diff --git a/v3-docs/docs/tutorials/svelte/9.next-steps.md b/v3-docs/docs/tutorials/svelte/9.next-steps.md
new file mode 100644
index 0000000000..fec785efbc
--- /dev/null
+++ b/v3-docs/docs/tutorials/svelte/9.next-steps.md
@@ -0,0 +1,17 @@
+## 9: Next Steps
+
+You have completed the tutorial!
+
+By now, you should have a good understanding of working with Meteor and Svelte.
+
+::: info
+You can find the final version of this app in our [GitHub repository](https://github.com/meteor/meteor3-svelte).
+:::
+
+Here are some options for what you can do next:
+
+- Check out the complete [documentation](https://v3-docs.meteor.com/) to learn more about Meteor 3.
+- Read the [Galaxy Guide](https://galaxy-support.meteor.com/en/article/deploy-to-galaxy-18gd6e2/) to learn more about deploying your app.
+- Join our community on the [Meteor Forums](https://forums.meteor.com/) and the [Meteor Lounge on Discord](https://discord.gg/hZkTCaVjmT) to ask questions and share your experiences.
+
+We can't wait to see what you build next!
diff --git a/v3-docs/docs/tutorials/svelte/assets/collections-connect-db.png b/v3-docs/docs/tutorials/svelte/assets/collections-connect-db.png
new file mode 100644
index 0000000000..6e29b8eb70
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/collections-connect-db.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/collections-documents.png b/v3-docs/docs/tutorials/svelte/assets/collections-documents.png
new file mode 100644
index 0000000000..3b7e0a2a99
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/collections-documents.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/collections-see-database.png b/v3-docs/docs/tutorials/svelte/assets/collections-see-database.png
new file mode 100644
index 0000000000..3a3115fe80
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/collections-see-database.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/collections-tasks-list.png b/v3-docs/docs/tutorials/svelte/assets/collections-tasks-list.png
new file mode 100644
index 0000000000..cc7924ca4f
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/collections-tasks-list.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/mermaid-diagram-svelte-rendering.png b/v3-docs/docs/tutorials/svelte/assets/mermaid-diagram-svelte-rendering.png
new file mode 100644
index 0000000000..9e8232b7f9
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/mermaid-diagram-svelte-rendering.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/mermaid-diagram-svelte-rendering.svg b/v3-docs/docs/tutorials/svelte/assets/mermaid-diagram-svelte-rendering.svg
new file mode 100644
index 0000000000..4cbd325e39
--- /dev/null
+++ b/v3-docs/docs/tutorials/svelte/assets/mermaid-diagram-svelte-rendering.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/v3-docs/docs/tutorials/svelte/assets/step01-dev-tools-mobile-toggle.png b/v3-docs/docs/tutorials/svelte/assets/step01-dev-tools-mobile-toggle.png
new file mode 100644
index 0000000000..4865e38742
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step01-dev-tools-mobile-toggle.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/step01-mobile-with-meta-tags.png b/v3-docs/docs/tutorials/svelte/assets/step01-mobile-with-meta-tags.png
new file mode 100644
index 0000000000..f864cc1815
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step01-mobile-with-meta-tags.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/step01-mobile-without-meta-tags.png b/v3-docs/docs/tutorials/svelte/assets/step01-mobile-without-meta-tags.png
new file mode 100644
index 0000000000..e23043ea91
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step01-mobile-without-meta-tags.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/step03-form-new-task.png b/v3-docs/docs/tutorials/svelte/assets/step03-form-new-task.png
new file mode 100644
index 0000000000..44cf04444c
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step03-form-new-task.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/step03-new-task-on-list.png b/v3-docs/docs/tutorials/svelte/assets/step03-new-task-on-list.png
new file mode 100644
index 0000000000..971d7a4c4c
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step03-new-task-on-list.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/step04-checkbox.png b/v3-docs/docs/tutorials/svelte/assets/step04-checkbox.png
new file mode 100644
index 0000000000..930425ac2d
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step04-checkbox.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/step04-delete-button.png b/v3-docs/docs/tutorials/svelte/assets/step04-delete-button.png
new file mode 100644
index 0000000000..29a8be7afa
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step04-delete-button.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/step05-styles.png b/v3-docs/docs/tutorials/svelte/assets/step05-styles.png
new file mode 100644
index 0000000000..790abbe8fe
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step05-styles.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/step06-all.png b/v3-docs/docs/tutorials/svelte/assets/step06-all.png
new file mode 100644
index 0000000000..e0934440f6
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step06-all.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/step06-ddp-messages.png b/v3-docs/docs/tutorials/svelte/assets/step06-ddp-messages.png
new file mode 100644
index 0000000000..6d54b038f4
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step06-ddp-messages.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/step06-extension.png b/v3-docs/docs/tutorials/svelte/assets/step06-extension.png
new file mode 100644
index 0000000000..0f7400a8be
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step06-extension.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/step06-filtered.png b/v3-docs/docs/tutorials/svelte/assets/step06-filtered.png
new file mode 100644
index 0000000000..8830409760
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step06-filtered.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/step07-login.png b/v3-docs/docs/tutorials/svelte/assets/step07-login.png
new file mode 100644
index 0000000000..6437e20ed8
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step07-login.png differ
diff --git a/v3-docs/docs/tutorials/svelte/assets/step07-logout.png b/v3-docs/docs/tutorials/svelte/assets/step07-logout.png
new file mode 100644
index 0000000000..a63d60d728
Binary files /dev/null and b/v3-docs/docs/tutorials/svelte/assets/step07-logout.png differ
diff --git a/v3-docs/docs/tutorials/svelte/index.md b/v3-docs/docs/tutorials/svelte/index.md
new file mode 100644
index 0000000000..6171827fc0
--- /dev/null
+++ b/v3-docs/docs/tutorials/svelte/index.md
@@ -0,0 +1,25 @@
+# Meteor.js 3 + Svelte
+
+In this tutorial, we will create a simple To-Do app using [Svelte](https://svelte.dev/) and Meteor 3.0. Meteor works well with other frameworks like [React](https://react.dev/), [Vue 3](https://vuejs.org/), [Solid](https://www.solidjs.com/), and [Blaze](https://www.blazejs.org/).
+
+Svelte is a modern UI framework that compiles your code to highly efficient vanilla JavaScript at build time, resulting in smaller bundles and faster runtime performance. Launched in 2016, it has gained popularity for its simplicity and reactivity without a virtual DOM. Compared to older approaches, Svelte eliminates much of the boilerplate and runtime overhead found in frameworks like React. It uses a declarative syntax with built-in state management, transitions, and stores that can be integrated with Meteor's reactive data sources like [Tracker](https://docs.meteor.com/api/tracker.html) and [Minimongo](https://docs.meteor.com/api/collections.html). This means your UI updates automatically as data changes, without manual DOM manipulation.
+
+If you're new and not sure what UI framework to use, Svelte is a great place to start—it's easy to learn, performant, and has a growing community. You can still leverage Meteor packages designed for other frameworks, like [accounts-ui](https://docs.meteor.com/packages/accounts-ui), even in a Svelte app.
+
+To start building your Svelte app, you'll need a code editor. If you're unsure which one to choose, [Visual Studio Code](https://code.visualstudio.com/) is a good option.
+
+Let’s begin building your app!
+
+# Table of Contents
+
+[[toc]]
+
+
+
+
+
+
+
+
+
+
diff --git a/v3-docs/docs/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.md b/v3-docs/docs/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.md
index 5b426a8599..60280b4656 100644
--- a/v3-docs/docs/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.md
+++ b/v3-docs/docs/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.md
@@ -33,7 +33,7 @@ If you encounter any issues, please refer to the requirements and details in [ou
To set up Meteor with Vue easily, run the following command, replacing `simple-todos-vue` with your chosen project name:
```shell
-meteor create --vue simple-todos-vue --release=3.0.2
+meteor create --vue simple-todos-vue
```
::: info
@@ -133,7 +133,7 @@ import Task from './components/Task.vue'
const getTasks = () => {
return [
- { _id: 1, text: 'Install Node.js 20' },
+ { _id: 1, text: 'Install Node.js' },
{ _id: 2, text: 'Install Meteor 3' },
{ _id: 3, text: 'Create and run your project' },
];
@@ -163,7 +163,7 @@ import Task from './components/Task.vue'
const getTasks = () => {
return [
- { _id: 1, text: 'Install Node.js 20' },
+ { _id: 1, text: 'Install Node.js' },
{ _id: 2, text: 'Install Meteor 3' },
{ _id: 3, text: 'Create and run your project' },
];