Meteor

Overview

Meteor (meteor.com) is an open-source, full-stack, JavaScript platform for building web and mobile (Android and iOS) applications. It is referred to as a platform instead of a framework because it works with other frameworks. It is used to host both client-side and server-side code.

Meteor is built on Node.js. It has great integration with the MongoDB NoSQL database. It also supports a publish and subscribe mechanism to synchronize user interfaces (UIs) with backend data, providing real-time updates.

Meteor uses its own build systems and JavaScript bundler. It does not use an external bundler such as Webpack, Rollup, or Parcel. This is partially because Meteor was created before those tools were created or became mature.

Meteor has its own frontend framework called Blaze. But it also supports other popular options that can be used in its place such as Angular, React, Svelte, Vue, and Cordova (for Android and iOS apps). (Cordova is an open source fork of PhoneGap which was created by Adobe. On August 10, 2020 Adobe announced they will no longer support development of PhoneGap. Development of Cordova continues under Apache.)

Meteor was initially released in 2012. It gained immediate attention for its novel use of WebSockets as an alternative to HTTP to achieve real-time updates. But the attention faded quickly as other frameworks garnered more notice. It is seeing some resurgence in 2020.

Tiny (tinycapital.com), a Canadian technology holding company, acquired Meteor from the Meteor Development Group in October, 2019. This occurred after most of the Meteor Development Group team transitioned to working on Apollo GraphQL. The new Meteor company supports both Meteor and Galaxy. Galaxy is a commercial cloud hosting platform for Meteor applications.

Companies that use Meteor include Qualcomm, Mazda, IKEA, and Honeywell.

Enterprise support is available in order to have guaranteed support response times and service-level agreements (SLAs).

Meteor provides many command-line tools summarized here. After installing Meteor, enter meteor help for help on using these.

Key Benefits

The key benefits of using Meteor are:

All of this enables implementing significant functionality in a small amount of code.

Installing

For platform-specific instructions on installing Meteor, see here. On Linux or macOS, enter curl https://install.meteor.com | sh. On Windows, install Chocolatey and enter choco install meteor.

Install Meteor also installs several tools used by it including MongoDB and TypeScript. Node.js must be installed separately and that includes the npm tool.

Repeating the install command will update the existing installation.

Creating a Project

To create the initial directory structure for a new Meteor project and run it, enter the following commands:

This is covered in more detail later in the "Todo App" section.

It is recommended to use the command meteor npm instead of npm when installing npm packages in a Meteor app. When packages have binary dependencies, this ensures that they are built using the same C libraries.

Meteor provides "hot code pushes" so the browser doesn't need to be manually refreshed and the server doesn't need to be manually restarted. After making changes to client code, the message "Client modified -- refreshing" will appear in the terminal window where the server is running. After making changes to server code, the message "Meteor server restarted" will appear in the same place.

Directory Structure

The recommended directory structure for Meteor applications and the initial set of files inside them is:

Additional files can be added to all the directories listed above except .meteor.

In the past Meteor eagerly loaded files outside the imports directory and lazily loaded files inside the imports directory. As of Meteor 1.7 it no longer makes this distinction and all code is lazily loaded. So there is no longer a requirement to have an imports directory. However, it is still a useful location for files that are shared between client and server code.

Meteor Packages

Meteor projects can use packages from npm and from its own package repository called "Atmosphere". Atmosphere contains packages that are specific to Meteor. These are referred to as "Isopacks" because they are typically isomorphic, meaning that the code can run on the client or server.

To see the available packages in Atmosphere, browse atmosphere.com. Packages without an author prefix followed by a colon are official Meteor packages. This page lists packages that are trending, recently added/updated, and most frequently used.

To install a package from Atmosphere in your current Meteor project, enter meteor add {package-name}. This writes information about the installed package to .meteor/packages in order to track dependencies, similar to how npm uses the package-lock.json file. It also installs packages on which this one depends. If the app is running, Meteor performs a "hot code push" of the CSS and JS files in the package so the effect is seen immediately, unlike installing npm packages.

After adding packages to a project, to update to the latest version of each, enter meteor update. This also updates to the latest version of Meteor. To only update packages and not Meteor, enter meteor update --packages-only. The version of each package being used can be found in the file .meteor/versions.

To import functions, classes, and other values from a Meteor package, use an import with the following syntax:

import {name1, name2} from 'meteor/{package-prefix}:{package-name}';

There are five types of packages:

  1. meteor-base
    This is a specific package that contains core components.
  2. first-party (core) packages
    These are bundled with Meteor. Some are included by default, but can be removed. Others are not, but can be added. Their names do not have an author prefix.
  3. local-packages
    These are specific to your app and reside in the packages directory.
  4. Atmosphere (community) packages
    Many of these follow the author:package naming convention and all that are in use are listed in the file .meteor/packages.
  5. npm packages
    These are listed as dependencies in the file package.json.

New Meteor apps typically include three core packages by default which are meteor-platform, autopublish, and insecure. The meteor-platform package has many dependencies that are also installed including blaze, check, ddp, jquery, mongo, session, tracker, and underscore.

Popular Atmosphere packages include:

MongoDB

MongoDB is a popular NoSQL database. There are many commands and options to master, but the following presents some of the basics for using the MongoDB console in conjunction with Meteor.

To start the MongoDB console, enter meteor mongo.

To see a list of all the databases, enter show dbs.

Meteor uses the database named "meteor". To interact with this database, enter use meteor.

To see a list of collections in the current database, enter show collections.

To see the first 20 documents in a given collection, enter db.{coll-name}.find().

To delete all the documents in a collection, enter db.{coll-name}.drop().

To reset the "meteor" MongoDB database, deleting all documents in all collections, stop the Meteor server and enter meteor reset. This can be useful to start an app in a pristine state.

Multiple Meteor apps can use the same MongoDB database to share data. Search for "Multiple apps use the same database" in the Meteor Up docs.

Collections

The client-side of Meteor applications can obtain data in many ways including REST calls and GraphQL queries that connect to any kind of database. The data can be stored in clients in many ways including JavaScript variables, session storage, and local storage.

The most common way for a Meteor application to store data is in "collections". These are typically associated with a MongoDB collection. They can be accessed from both client and server code. When data is added to a collection, updated, or deleted, client code that uses it typically updates automatically. This provides a significant benefit over using REST services that require the use of polling to get data updates.

The following code demonstrates creating a MongoDB collection named "tasks", inserting a document into the collection, and finding it. By convention, variable names that refer to collections have CamelCase names that start uppercase. MongoDB stores the unique id of each document in property named _id. If no value is provided, MongoDB will generate a unique value.

const Tasks = new Mongo.Collection('tasks');
const id = Tasks.insert({text: 'buy milk'}); // returns assigned id
const task = Tasks.findOne({_id: id});

The same code can be used in client or server code when the Meteor "insecure" package is installed. The starter app includes this. Later we will look at securing Meteor applications by removing this package.

If the code above is run in the server, it creates a MongoDB collection (if it doesn't already exist) and populates it. If the code above is run in the client, it creates a client-side cache using the "Minimonogo" library which provides an in-memory JavaScript implementation of the MongoDB API.

Minimongo in the client is automatically kept in sync with MongoDB in the server when the "autopublish" package is installed. The starter app includes this. Later we will look at removing this.

When "autopublish" is not installed, the server must publish documents from collections and the client must subscribe documents of interest. This keeps the client-side cache (implemented by Minimongo) in sync with a subset of the data in MongoDB on the server. The underlying mechanism that supports this is the Meteor Distributed Data Protocol (DDP) which sends WebSocket messages in both directions.

Local Collections

It is also possible to create a "local collection" that only exists on the client or on the server. These are Minimongo collections that are not synchronized with a MongoDB collection. They are useful for data that only resides in memory in cases where using the MongoDB API to access it is convenient. An alternative is to use a ReactiveVar, ReactiveDict, or the Session object, all of which are described later.

To create a local collection:

const myCollection = new Mongo.Collection(null);

or

const myCollection = new Mongo.Collection('some-name', {connection: null});

To support associations between documents in different collections, see the Meteor package cultofcoders:grapher.

Collection Schemas

A schema can be associated with a collection in order to provide validation when documents are added or updated. Schemas can be described using the npm package simpl-schema, which is installed by entering npm install simpl-schema.

The type of each property can be specified with either a shorthand or longhand definition. Shorthand definitions only specify a type. Longhand definitions can also specify that a property is optional, a default value, a max length, a regular expression (regEx) to be matched, and more. Both kinds of definitions can be used in the same schema, one per property.

For example, a tasks collection can be associated with a schema as follows:

import SimpleSchema from 'simpl-schema';

const Tasks = new Mongo.Collection('tasks');
Tasks.schema = new SimpleSchema({
createdAt: {type: Date, defaultValue: new Date(), optional: true},
done: {type: Boolean, defaultValue: false, optional: true},
owner: String,
text: String,
username: String
});

Note that specifying a defaultValue for a property does not imply that it is optional. That must also be specified.

Here is another example where a property (topics) is an array of objects:

import SimpleSchema from 'simpl-schema';

export const Meetings = new Mongo.Collection('meetings');
const topicSchema = new SimpleSchema({
description: {type: String, defaultValue: ''},
presenter: {type: String, defaultValue: ''},
minutes: {type: Number, defaultValue: 0}
});
Meetings.schema = new SimpleSchema({
name: {type: String, defaultValue: ''},
date: String,
time: String,
duration: SimpleSchema.Integer,
topics: [topicSchema]
});

Schema validation is not performed automatically. It is done by explicitly passing an object to the validate method. For example, if Tasks.schema.validate(newTask) is called and the object referred to by newTask does not match the schema, a ValidationError which describes the difference is thrown. Extra properties in the object that are not specified in the schema are not allowed.

The aldeed:collection2 Meteor package can be used to automatically validate every insert, update, and upsert performed on a given collection. In addition it coerces values to the expected types when possible, removes properties not in the schema, and assigns default values specified with the defaultValue property. To install this package, enter meteor add aldeed:collection2. To use this on our Tasks and Meetings collections:

Tasks.attachSchema(Tasks.schema);
Meetings.attachSchema(Meetings.schema);

For more detail on schema definitions, see simpl-schema.

Creating collections whose documents can be deeply nested or can contain large arrays is not recommended. This is because DDP, which is used to send messages to clients that describe new and updated documents, is only able to describe top-level properties and their values. For example, if a document contains a property whose value is a large array and a single element is modified, DDP will send the entire array to clients. This works fine, but is inefficient.

Meteor Methods

Meteor Methods are functions that can reside only on the server or on both the client and server. They are meant to be called from client code. The server instance is invoked using a Remote Procedure Call (RPC) that utilizes WebSockets. By convention, "Method" is written with a capital "M" to distinguish it from normal JavaScript methods.

Meteor Methods are an alternative to REST calls implemented using HTTP. An issue with using Meteor Methods instead of REST is that they can only be called from the same Meteor app. In many cases the Methods are specific to the app and this is a non-issue.

Common uses for Methods include inserting a document into a collection, deleting a document from a collection, and updating a document in a collection. There is typically no need to return anything to the client because changes in the MongoDB database are communicated back to the client through WebSocket messages that keep Minimongo in the client synchronized. Retrieving documents from collections is typically done by subscribing to them rather than through methods calls, but Methods can also be used for this purpose.

Methods can also be used to perform CRUD operations on SQL databases. In this case it makes sense for the methods to only reside on the server and for them to return data needed by the client.

To implement Methods, import Meteor from the meteor/meteor package and pass an object literal to Meteor.methods. The properties of the object are Method definitions. This registers the Methods with Meteor's DDP system. The Meteor.methods function can be called any number of times to register additional Methods.

Methods can accept any number of parameters with any JavaScript types.

The check function in the meteor/check package can be used to check the types of parameters at runtime. Note that TypeScript types are only checked at compile-time. The check function throws a Match.Error error if unexpected types are passed. For details on the check function, see here.

To report other kinds of parameter validation errors that are based on values rather than types, use throw new ValidationError(message).

To report most other errors, use throw new Meteor.Error(errorId, message[, details]). The error id is typically the Method name followed by a period and an error name. If a generic JavaScript error is thrown instead using throw new Error(message), the details will only appear on the server and clients will only be notified that an internal server error occurred.

To call a Method from client code, import Meteor from the meteor/meteor package and call Meteor.call, passing it the name of a Method, any number of arguments, and a callback function. This triggers the Method call lifecycle which has six parts.

  1. If the Method is defined in the client, it is executed there. This typically updates Minimongo and corresponding UI updates are made in response to these changes.
  2. A JSON-based DDP message is constructed and sent to the server using a WebSocket connection. This includes a message name (msg property set to "method"), Method name (in the method property), arguments (in the params property), and a generated Method id (in the id property).
  3. The Method executes on the server, possibly updating MongoDB.
  4. A return value is sent to the client in a message that includes a message name (msg property set to "result"), the previously generated Method id (in the id property), and the result (in the result property).
  5. DDP messages that describe changes made in MongoDB in the server are received in the client and used to make corresponding changes in Minimongo, but the UI is not yet updated.
  6. The callback function passed to Meteor.call is invoked, passing it an error description (if there was an error) and the result value. Now the UI is updated using data from Minimongo.

If the call succeeds, the error description is undefined and the result is set to whatever value the Method returns. If the call fails, the error description is set to an object that contains the properties error (an HTTP status code even though HTTP isn't used), reason (concise message), message (long message), and more. It does not throw a JavaScript Error, so try/catch cannot be used.

If a Method inserts new documents in a collection, Meteor uses the same random generator seed on the client and server in order to guarantee that documents created by client calls are assigned the same _id values as documents created by server calls.

When multiple Methods are called without waiting for the result of each before calling the next, Meteor guarantees that they will execute in the order in which they are called and that the results will be processed in the same order. In some cases you may wish to allow multiple methods to execute concurrently. Call this.unblock() in a Method implementation to allow another Method to begin executing before that one finishes.

While it is not commonly needed, Meteor supports Method implementations that call other Methods.

Let's implement a simple Method that just adds two numbers. It is not a typical Method because it does not update a collection. We will see several examples of Methods that update a collection later. To verify that the Method is called on both the client and server side, a console.log call is added. It will be output in the DevTools console AND in the terminal window where the server is running.

Create the directory imports and the file imports/methods.js containing the following:

import {check} from 'meteor/check';
import {Meteor} from 'meteor/meteor';

Meteor.methods({
sum(n1, n2) {
console.log('sum called; on server?', Meteor.isServer);
check(n1, Number); // parameter type validation
check(n2, Number); // parameter type validation
return n1 + n2;
}
});

Add the following near the top of client/main.js and server/main.js to invoke the code above in the client and server:

import '../imports/methods.js';

Add the following in client/main.js to call the sum Method:

const number1 = 2;
const number2 = 3;
Meteor.call('sum', number1, number2, (err, result) => {
if (err) {
alert(err);
} else {
console.log('sum =', result); // 5
}
});

In this case there is no benefit to invoking the method on both the client and server because it doesn't rely on any server resources. Later in the "Optimistic UI" section we will learn why this can be useful.

If you prefer to use promises instead of callback functions, implement a utility function that wraps calls in a Promise as follows:

export function call(name, ...args) {
return new Promise((resolve, reject) => {
Meteor.call(name, ...args, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}

An advantage of calling Methods in this way is that the result of one call can be used to form the arguments of another call without resorting to nested callback functions.

Using this new call function, our sum Method can be called from an async function as follows:

try {
const sum = await call('sum', 2, 3);
console.log('App.svelte addTask: sum =', sum);
} catch (e) {
alert(e);
}

If a Method returns a Promise, Meteor will wait for the Promise to resolve or reject before returning a result or an error to the client.

Two alternate ways to define Methods are Advanced Method boilerplate and mdg:validated-method. These provide additional features, but may be overkill for most Methods.

Typically client code only invokes methods that are registered in the server of the same Meteor application. However, it is also possible to invoke methods defined in the server of a different Meteor application. To do this:

import {DDP} from 'meteor/ddp-client';

const connection = DDP.connect(serverUrl);
connection.call(methodName, arguments, callback);

For more details, see https://docs.meteor.com/api/connections.html#DDP-connect.

Method Retries

If a client calls a Method and their internet connection is lost before a result is returned, Meteor will remember this and will make the call again when connectivity is restored. This presents an issue when Methods are not idempotent. In this context, being idempotent means that when a call is repeated no additional changes are made in the database.

Consider typical CRUD operations.

Optimistic UI

Optimistic UI is a feature of Meteor Methods, and also of Apollo GraphQL, that enables a UI to respond to user interactions without waiting for server responses. This is satisfied by the following properties.

  1. The UI is rendered on the client rather than waiting for the server to return HTML or data. This requires predicting the result of Method calls based on data that has been cached on the client.

  2. The client-side cache (stored in Minimongo) is a global cache rather than being associated with specific components, so it is not possible for components to disagree on the state. All affected components can re-render using the same data. When a MongoDB query is executed using a Meteor Method, it is first run against Minimongo on the client and then against MongoDB on the server.

  3. The client subscribes to data using DDP to keep Minimongo on the client in sync with MongoDB on the server. Updates are made in real time, not using polling.

  4. When the server returns the actual results of a Method call, Meteor verifies that it matches what the client predicted. If they differ, Meteor rolls back all the changes made in Minimongo from that point forward and applies the correct changes from the server. The UI can then update using the new data in Minimongo.

All of this functionality is provided by default. The only requirement is for the client to use Meteor Methods to request data changes.

The following sequence diagram illustrates the order of these operations. It assumes that Svelte is being used to implement the UI and that data retrieved from Minimongo is placed in a Svelte store.

Meteor Method flow

If the Method throws an error when run on the client, it is not called on the server.

To see rollback in action, we can modify a Meteor Method to return a different result when run on the client versus the server. This is not typical, but it is useful to demonstrate optimistic UI.

Here is a simplified example from the upcoming Todo app. It inserts a document in the "tasks" collection. When run on the client it updates Minimongo using the text passed as an argument. When run on the server it uses a timeout to wait three seconds and then uses the uppercase version of the text. The Method first runs on the client and the UI renders a new task with the entered text. The Method then runs on the server. After the timeout it sends a message to the client that tells it to update Minimongo using the uppercase version of the text. The UI then updates to show the uppercase text.

Meteor.methods({
addTask(text) {
check(text, String);
return new Promise(resolve => {
if (Meteor.isServer) {
// Meteor requires using this version of setTimeout.
// See https://docs.meteor.com/api/timers.html.
Meteor.setTimeout(() => {
// Tasks is a collection.
const id = Tasks.insert({text: text.toUpperCase()});
resolve(id);
}, 3000);
} else {
// in client
const id = Tasks.insert({text});
resolve(id);
}
});
}
});

It is possible to define a Method only on the server-side, thereby opting out of optimistic UI. The UI will not update until the Method finishes executing on the server and sends back a result message. This is because Minimongo will not be updated until the result message is received.

Here is an example of defining a Method on the server taken from the upcoming Todo app. It deletes a task with a given id.

if (Meteor.isServer) {
Meteor.methods({
deleteTask(taskId) {
check(taskId, String);
Tasks.remove(taskId);
}
});
}

Viewing WebSocket Messages

Chrome DevTools can be used to view the WebSocket messages that are sent by Meteor. Like HTTP messages, they appear on the Network tab. But unlike HTTP messages where there is a separate entry for each request/response pair, there is only a single WebSocket entry per connection. Selecting a connection displays all the messages sent over that connection.

To view WebSocket messages:

  1. Open the Chrome DevTools.
  2. Select the "Network" tab.
  3. Click "WS" to filter on WebSockets.
  4. Refresh the browser to get a new WebSocket connection.
  5. Click the WebSocket connection that is displayed.
  6. Click the "Messages" tab to see all the WebSocket messages. This list is updated as new messages are received.
  7. Click a message to see its details.

Each message is preceded by an arrow. Outgoing messages have a green arrow pointing up and are sent from the client to the server. Incoming messages have a red arrow pointing down and are sent from the server to the client.

Meteor WebSocket Messages

Each Meteor WebSocket message contains an array of JSON objects that have a msg property and other properties such as collection, fields, id, method, methods, msg, and params. Typically each message array contains a single object.

When a client initially connects to the server, a series of messages are sent from the server to the client. While I can't describe the purpose of each of these messages, I can describe the pattern observed. These messages include:

Periodically, about every 45 seconds, each side sends a "ping" message to verify that the other side is still reachable. The other side replies with a "pong" message.

If the Meteor accounts packages are used to support account creation and sign in, a sign in request triggers a message from the client to the server containing the following JSON:

{
"msg": "method",
"method": "login",
"params": [
{
"user": "{username}",
"password": "{hashed-password}",
"algorithm": "sha-256"
}
]
}

A successful sign in triggers a message from the server to the client containing the following JSON:

{
"msg": "result",
"id": "{message-id}",
"result": {
"id": "{result-id}",
"token": "{token}",
"token-expires": {"$date": "{timestamp}"},
"type": "password"
}
}

Updates to the MongoDB database trigger additional JSON messages from the server to the client that describe the changes. This allows Minimongo in the client to stay in sync. Each message has a msg property that specifies the type of update, a collection property set to the name of the collection, and an id property set to the _id value of the affected document.

When a document is added to a collection, msg is set to "added" and fields is set to a JSON object containing all the properties of the document.

When a document is updated, msg is set to "changed" and fields is set to a JSON object containing only the properties of the document that were modified.

When a document is deleted from a collection, msg is set to "remove".

When a client subscribes to a collection, "added" messages described above are sent from the server to initially populate Minimongo.

Tracker

Tracker is a dependency tracking system used by Meteor to update UIs when reactive variables and data sources change. From the Meteor Tracker docs, "When you call a function that supports reactive updates (such as a database query), it automatically saves the current Computation object, if any (representing, for example, the current template being rendered). Later, when the data changes, the function can "invalidate" the Computation, causing it to rerun (re-rendering the template)."

There is a low-level API for using Tracker, but there are easier ways to use it in React and Svelte.

For React there is hook called useTracker for responding to tracker changes. This is provided by the Meteor package react-meteor-data.

For Svelte, there is a useTracker function. This is provided by the Meteor package rdb/svelte-meteor-data.

For example, to always get the current user in a Svelte component when the accounts-password package is used:

$: user = useTracker(() => Meteor.user());

The rdb/svelte-meteor-data package also turns MongoDB cursor objects into Svelte stores that automatically update whenever the database is updated in a way that affects the query results.

For example, to always get the latest tasks from a task collection:

// A query can limit the documents returned,
// instead of getting all documents in the collection.
// It is not typically desirable to
// copy a large amount of data into Minimongo.
const query = {};
// A projection can limit the document properties returned
// and/or specify a sort order.
const projection = {sort: {createdAt: -1}}; // newest first
$: tasks = Tasks.find(query, projection);

Note that both user and tasks are Svelte stores, so references to them should have a $ prefix.

For more information on the relationship between useTracker and Svelte stores, see this issue.

ReactiveVar

A ReactiveVar is a client-side object holds a single value. They can be created and exported from .js files so they can be imported by multiple components. They can also be passed as a prop to components.

To use this feature, add the reactive-var package by entering meteor add reactive-var.

To create a new ReactiveVar object:

import {ReactiveVar} from 'meteor/reactive-var';

const myVar = new ReactiveVar(initialValue);

To change the value, call myVar.set(newValue).

To get the value, call myVar.get().

Here is a Svelte component that demonstrates using a ReactiveVar:

<script>
import {ReactiveVar} from 'meteor/reactive-var';

const myRV = new ReactiveVar(0);
let counter;
Tracker.autorun(() => {
counter = myRV.get();
});
</script>

<div>
counter = {counter}
<button on:click={() => myRV.set(counter + 1)}>Increment</button>
</div>

Since Svelte components already support reactive variables within components and stores to achieve reactivity across components, there is little reason to use ReactiveVar in Svelte components.

ReactiveDict

A ReactiveDict is a client-side reactive data store that holds key/value pairs. They are used in the same ways a ReactiveVar objects, but they can hold multiple values instead of just one. The values can be any kind of JavaScript value that can be converted to JSON. Other values can be made compatible by calling EJSON.addType(name, factory).

Multiple instances can be created to maintain separate groups of related key/value pairs.

To use this feature, add the reactive-dict package by entering meteor add reactive-dict.

To create a new ReactiveDict object:

import {ReactiveDict} from 'meteor/reactive-dict';

const myDict = new ReactiveDict(name, initialKeyValuePairs);

Both constructor arguments are optional. Providing a name allows the key/value pairs to survive hot code pushes, but not browser refreshes. Providing an initial value is an alternative to starting with no key/value pairs.

To set the default value of a ReactiveDict key, call ReactiveDict.setDefault(key, value). Only the first call is processed. Subsequent calls are ignored.

To set a new value of a ReactiveDict key, call ReactiveDict.set(key, value).

To get the value of a ReactiveDict key, call ReactiveDict.get(key).

To get all the current key/value pairs, call myDict.all().

To remove all the key/value pairs, call myDict.clear().

For the same reasons explained in the previous section on ReactiveVar, there is little reason to use ReactiveDict in Svelte components.

Session

The Session is a client-side, global ReactiveDict object. It is typically used to share data between components. The Session object cannot be shared between users or between browser tabs.

To use this feature, add the session package by entering meteor add session.

To use Session in a component, import it with import {Session} from 'meteor/session;

The API for the Session object is identical to that of ReactiveDict objects since it is one.

To make the Session reactive so code is executed whenever the value for a given key changes, use Tracker.autorun as follows:

let someValue;
Tracker.autorun(() => {
someValue = Session.get(key);
});

Here is a Svelte component that demonstrates using a Session:

<script>
import {Session} from 'meteor/session';
import {Tracker} from 'meteor/tracker';

let counter;
Tracker.autorun(() => {
counter = Session.get('counter');
});
Session.setDefault('counter', 0);

function increment() {
Session.set('counter', counter + 1);
}
</script>

<div class="container">
{counter}
<button on:click="{increment}">Increment</button>
</div>

The ESLint plugin eslint-plugin-meteor will complain about the use of Session based on the meteor/no-session rule. Disable this ESLint rule if you decide to use Session.

For the same reasons explained in the previous section on ReactiveVar, there is little reason to use Session in Svelte components.

Email

TODO: Document the Meteor API for sending email.

ESLint

The steps to install and configure the ESLint linting tool for use in a Meteor project are listed below. They assume using Svelte as the web framework. When this is not the case, the Svelte-specific parts can be omitted.

To run ESLint, enter npm run lint.

ESLint will give "Unable to resolve path to module" errors on all imports that begin with "meteor/". However, the Meteor build system is able to resolve these. To suppress these errors, add the following top level property in the .eslintrc.json file:

"rules": {
"import/no-unresolved": ["error", {"ignore": ["^meteor/"]}]
}

Prettier

The steps to install and configure the Prettier code formatting tool for use in a Meteor project are listed below. They assume using Svelte as the web framework. When this is not the case, the Svelte-specific parts can be omitted.

To run Prettier, enter npm run format.

DevTools

The Chrome extension "Meteor DevTools Evolved" displays DDP messages, Minimongo contents, and subscriptions.

To install it, browse here.

Tutorials

The Meteor web site contains several tutorials here that provide introductions to using Meteor.

Todo App

Let's walk through the steps to build a Todo app using Meteor. This is a modified version of the Todo app presented at the Meteor site that demonstrates additional features. Code for the final version of this app can be found in GitHub.

  1. Create the app starting point by entering meteor create todos.

    We are using the default app template here. To use a non-default app template, add one of the following options after create: --bare, --minimal, --full, --react, or --typescript. None of these options corresponds to the default which produces applications that are insecure. Such applications allow all MongoDB updates to be initiated from clients and all documents are synced from MongoDB on the server to Minimongo in the client. We will learn how to fix this later. For details on the Meteor packages included by default and with each of the options see here.

  2. Modify the start script in package.json as follows if you are not targeting older browsers or mobile devices in order to avoid generating unnecessary code:

    "start": "meteor run --exclude-archs 'web.browser.legacy, web.cordova'",
  3. Start the server by entering cd todos, meteor npm install, and npm start.

  4. Browse localhost:3000 to see the following page:

    default Meteor app

  5. Press the "Click Me" button multiple times.

    The text "You've pressed the button n times." will update to display the number of times it was clicked.

  6. Exercise "hot code push".

    The UI is defined by the following files in the client directory: main.html, main.css, and main.js. Try editing the file client/main.html. The browser will updated automatically when the changes are saved.

  7. Configure the app to use the Svelte web framework, instead of the default Blaze framework.

    Svelte is a good choice due to its use of reactive statements. For more information about Svelte, see my article.

    • Install Svelte by entering meteor npm install svelte.

    • Add some Meteor packages by entering
      meteor add svelte:compiler rdb:svelte-meteor-data

    • Remove a package that will no longer be used by entering
      meteor remove blaze-html-templates

    • Add a replacement Meteor package by entering
      meteor add static-html

  8. Replace the content of client/main.html with the following:

    <head>
    <title>Todo App</title>
    </head>
    <body>
    <!-- Svelte will insert content in this div. -->
    <div id="app"></div>
    </body>
  9. Replace the content of client/main.js with the following:

    import {Meteor} from 'meteor/meteor';
    import App from './App.svelte';

    Meteor.startup(() => {
    new App({target: document.getElementById('app')});
    });
  10. Create the file client/Task.svelte containing the following:

    <script>
    export let task; // Parent components pass this value as a prop.

    const formatDate = date =>
    date.toLocaleDateString('en-us', {
    month: 'short',
    day: 'numeric',
    year: 'numeric'
    });

    // These are Svelte "reactive declarations" which are
    // re-executed any time a variable they depend on changes.
    $: ({createdAt, text} = task);
    $: item = text + (createdAt ? ` - added ${formatDate(createdAt)}` : '');
    </script>

    <li>{item}</li>

    <style>
    /* This styling is scoped to this component. */
    li {
    padding-left: 0;
    }
    </style>
  11. Create the file client/App.svelte containing the following:

    <script>
    import Task from './Task.svelte';

    // Later we will replace this with a way to get the tasks from MongoDB.
    function getTasks() {
    const createdAt = new Date();
    return [
    {_id: 1, text: 'This is task 1', createdAt},
    {_id: 2, text: 'This is task 2', createdAt},
    {_id: 3, text: 'This is task 3', createdAt}
    ];
    }
    </script>
    <div class="container">
    <header>
    <h1>Todo App</h1>
    </header>
    <section>
    <ul>
    {#each getTasks() as task}
    <Task {task} />
    {/each}
    </ul>
    </section>
    </div>

    At this point the app should display the following basic task list:

    Todo App after step 14

  12. Replace the contents of client/main.css with what is found at here. Now the task list is styled nicely.

    Todo App after step 15

  13. Create a top-level project directory named imports.

  14. Create the file imports/tasks.js containing the following which will create a MongoDB collection named "tasks":

    import {Mongo} from 'meteor/mongo';

    export const Tasks = new Mongo.Collection('tasks');
  15. Add the following import near the top of server/main.js.

    import '../imports/tasks.js';
  16. Add the following import near the top of client/App.svelte:

    import {Tasks} from '../imports/tasks.js';
  17. Replace the getTasks function in client/App.svelte with the following in order to get tasks from MongoDB:

    const query = {};
    const projection = {sort: {createdAt: -1}}; // newest first
    // This is a MongoDB cursor which is also a Svelte store.
    $: tasks = Tasks.find(query, projection);
  18. Replace the call to getTasks() in client/App.svelte with $tasks.

    This is a reference to a Svelte store that is kept in sync with the MongoDB "tasks" collection. The UI will no longer display any tasks because this collection does not yet contain any tasks.

  19. Insert some tasks using the MongoDB console by entering the following:

    meteor mongo
    db.tasks.insert({ text: "buy milk" })
    db.tasks.insert({ text: "take out trash" })

    Note that the UI updates automatically to show these new tasks.

    Todo App after step 22

  20. Enable adding tasks in the UI.

    • Add the following inside the script element in client/App.svelte:

      let text = '';

      function addTask() {
      Tasks.insert({text, createdAt: new Date()});
      text = '';
      }
    • Add the following in client/App.svelte as the first child of the section element to prepare for adding tasks in the UI:

      <form on:submit|preventDefault="{addTask}">
      <input placeholder="todo text" bind:value="{text}" />
      <button>Add</button>
      </form>
    • Add the following after the HTML in client/App.svelte:

      <style>
      form
      {
      margin-top: 0;
      padding-bottom: 1rem;
      }

      section {
      padding: 1rem;
      }
      </style>

    Now new tasks can be added by entering text in the input and either pressing the "Add" button or pressing the return key.

    Todo App after step 25

  21. Note how Meteor keeps all clients in sync.

    To see this, open a second web browser or another window in the same web browser and browse localhost:3000. Add a task in either browser window and notice that it appears in both.

  22. Add the ability to mark tasks as done and delete them by changing client/Task.svelte to match the following:

    <script>
    import {Tasks} from '../imports/tasks.js';

    export let task;

    function deleteTask() {
    Tasks.remove(task._id);
    }

    const formatDate = date =>
    date.toLocaleDateString('en-us', {
    month: 'short',
    day: 'numeric',
    year: 'numeric'
    });

    function toggleDone() {
    Tasks.update(task._id, {$set: {done: !task.done}});
    }

    $: ({createdAt, text} = task);
    $: item = text + (createdAt ? ` - added ${formatDate(createdAt)}` : '');
    </script>

    <li>
    <input type="checkbox" checked="{task.done}" on:click="{toggleDone}" />
    <span class:done="{task.done}">{item}</span>
    <!-- using Unicode trash can -->
    <button on:click="{deleteTask}">&#x1f5d1;</button>
    </li>

    <style>
    button {
    background-color: transparent;
    border: none;
    }

    .done {
    opacity: 0.3;
    text-decoration: line-through;
    }

    li {
    padding-left: 0;
    }
    </style>

    Now tasks can be marked as done by clicking the checkbox in front of them and they can be deleted by clicking the trash can icon after them.

    Todo App after step 26

  23. Show the number of remaining tasks and total tasks in the heading by making the following changes in client/App.svelte:

    • Add the following in the script element:

      $: remaining = $tasks.filter(t => !t.done).length;
    • Add the following as the new first child of the section element:

      <p class="stats">{remaining} of {$tasks.length} remaining</p>
    • Add the following inside the style element:

      .stats {
      margin-top: 0;
      }

    Now when tasks are added, toggled between done and not done, and deleted, the number of remaining tasks and total tasks is updated.

    Todo App after step 27

  24. Add the ability to only display tasks that are not done by making the following changes in client/App.svelte:

    • Add the following variable declaration in the script element:

      let hideCompleted = false;
    • Add the following inside the form element after the "Add" button:

      <label className="hide-completed">
      <input type="checkbox" bind:checked="{hideCompleted}" />
      Hide Completed Tasks
      </label>
    • Change the content of the {#each} block to the following:

      {#if !hideCompleted || !task.done}
      <Task {task} />
      {/if}

    Now when the "Hide Completed Tasks" checkbox is checked, only tasks that are not done are displayed.

    Todo App after step 28

  25. Add support for user accounts by doing the following:

    • Add some Meteor packages by entering
      meteor add accounts-ui accounts-password svelte:blaze-integration

    • Create the file client/accounts-config.js with the following content:

      import {Accounts} from 'meteor/accounts-base';

      // Email is needed for the next step.
      Accounts.ui.config({
      passwordSignupFields: 'USERNAME_AND_EMAIL'
      });
    • Add the following near the top of client/main.js:

      import './accounts-config.js';

      Alternatively, this can be imported from a .svelte file, perhaps the one that renders <BlazeTemplate template="loginButtons" />, which is described next.

    • Modify the client/App.svelte file to match the following:

      <script>
      import {Meteor} from 'meteor/meteor';
      import {useTracker} from 'meteor/rdb:svelte-meteor-data';
      import {BlazeTemplate} from 'meteor/svelte:blaze-integration';
      import {Tasks} from '../imports/tasks.js';
      import Task from './Task.svelte';

      let hideCompleted = false;
      let text = '';
      let user;

      // user is a store
      $: user = useTracker(() => Meteor.user());

      const query = {};
      const projection = {sort: {createdAt: -1}}; // newest first
      // This is a MongoDB cursor which is also a Svelte store.
      $: tasks = Tasks.find(query, projection);

      $: remaining = $tasks.filter(t => !t.done).length;

      function addTask() {
      Tasks.insert({
      text,
      createdAt: new Date(),
      owner: Meteor.userId(),
      username: $user.username
      });
      text = '';
      }
      </script>

      <div class="container">
      <!-- This provides the UI for user account management. -->
      <BlazeTemplate template="loginButtons" />

      <header>
      <h1>Todo App</h1>
      </header>

      <section>
      {#if $user}
      <p class="stats">{remaining} of {$tasks.length} remaining</p>

      <form on:submit|preventDefault="{addTask}">
      <input placeholder="todo text" bind:value="{text}" />
      <button>Add</button>
      <label className="hide-completed">
      <input type="checkbox" bind:checked="{hideCompleted}" />
      Hide Completed Tasks
      </label>
      </form>

      <ul>
      {#each $tasks as task} {#if !hideCompleted || !task.done}
      <Task {task} />
      {/if} {/each}
      </ul>
      {:else}
      <p>Please sign in.</p>
      {/if}
      </section>
      </div>

      <style>
      form {
      margin-top: 0;
      padding-bottom: 1rem;
      }

      section {
      padding: 1rem;
      }

      .stats {
      margin-top: 0;
      }

      /* This is a way to define global styles from a Svelte component. */
      :global(#login-buttons) {
      padding: 0.5rem;
      text-align: right;
      }

      :global(#login-buttons a) {
      text-decoration: none;
      }

      :global(#login-dropdown-list) {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      }
      </style>

    Now users must create an account and sign in before they can view, add, and modify tasks. Information about user accounts is stored in the MongoDB database "meteor" inside the collection "users".

    Before a user signs in, they see a "Sign in" link in the upper-right.

    Todo App before sign in

    Clicking "Sign in" displays a dialog that contains a link to create an account.

    Todo App before sign in

    Clicking "Create account" changes the dialog to prompt for account information. By default, passwords must contain at least six characters, but there are no restrictions on the characters.

    Todo App before sign in

    After creating an account, or entering a username and password for an existing account, the user is signed in and the "Sign in" link in the upper-right is replaced by their username.

    Todo App before sign in

    Clicking the username link in the upper-right presents a dialog for signing out or changing the password.

    Todo App before sign in

    Clicking "Change password" changes the dialog to prompt for the current and new passwords.

    Todo App before sign in

    After successfully changing the password, the dialog changes to notify the user.

    Todo App before sign in

    For more information on users and accounts features and ways to customize them, see here. This includes adding support for OAuth logins and changing password validation rules. To give users role-based permissions and per-document permissions, see here.

  26. Add support for "forgot password" emails.

    This will allow users to click the "Forgot password" link in the "Sign in" dialog which changes the dialog content to prompt for an email address. If a user account exists for the email address, an email is sent containing a link that the user can click to reset their password. After doing so they are immediately signed in.

    • Add another Meteor packages by entering
      meteor add email

    • Create the file secrets.json in the project root directory with content like the following:

      {
      "MAIL_NAME": "Your Name",
      "MAIL_USER": "some-user",
      "MAIL_PASSWORD": "some-password",
      "MAIL_USER_DOMAIN": "some-company.com",
      "MAIL_SERVER_DOMAIN": "smtp.gmail.com",
      "MAIL_SERVER_PORT": 587
      }

      Note that the last two values above are specific to using a Google G Suite account for sending email. The values will differ if you are using a different email service. Another service to consider is MailChimp.

    • If you are saving the project in a Git repository, add secrets.json to the .gitignore file so it is not added to the repository for others to see.

    • Create the file server/accounts-setup.js with the following content:

      import {Accounts} from 'meteor/accounts-base';
      import secrets from '../secrets.json';

      const siteName = 'Todos by Mark';
      Accounts.emailTemplates.siteName = siteName;

      const from = `${secrets.MAIL_NAME}<${secrets.MAIL_USER}@${secrets.MAIL_USER_DOMAIN}>`;
      Accounts.emailTemplates.from = from;
    • Modify the file server/main.js to match the following:

      import {Meteor} from 'meteor/meteor';
      import '../imports/tasks.js';
      import './accounts-setup.js';
      import secrets from '../secrets.json';

      Meteor.startup(() => {
      // code to run on server at startup
      process.env.MAIL_URL =
      'smtp://' +
      secrets.MAIL_USER +
      '%40' +
      secrets.MAIL_USER_DOMAIN +
      ':' +
      secrets.MAIL_PASSWORD +
      '@' +
      secrets.MAIL_SERVER_DOMAIN +
      ':' +
      secrets.MAIL_SERVER_PORT;
      });

    Now when a user clicks the "Forgot password" link in the sign in dialog, they can enter their email address and press the "Reset password" button.

    Todo App forgot password

    When the email is sent, the dialog changes to include the text "Email sent".

    Todo App forgot password email sent

    The user will receive an email like the following:

    Todo App password reset email

    Clicking the link opens the app in a new browser tab and displays a dialog for resetting the password.

    Todo App password reset

    After entering a new password, the user is notified that it has been reset and that they are now logged in.

    Todo App after password reset

  27. Make the app more secure by moving database interactions to the server.

    This is accomplished by implementing Meteor Methods that are invoked by client-side code with calls to Meteor.call(name, data, callback).

    • Remove the Meteor package that allows client code to interact with MongoDB by entering meteor remove insecure.

    • Define Methods by modifying imports/tasks.js to match the following:

      import {check} from 'meteor/check';
      import {Meteor} from 'meteor/meteor';
      import {Mongo} from 'meteor/mongo';

      export const Tasks = new Mongo.Collection('tasks');

      Meteor.methods({
      addTask(text) {
      check(text, String); // parameter type validation

      // Make sure the user is logged in before inserting a task.
      if (!this.userId) throw new Meteor.Error('addTask', 'not-authorized');

      const {username} = Meteor.users.findOne(this.userId);
      const id = Tasks.insert({
      text,
      createdAt: new Date(),
      owner: this.userId,
      username
      });
      return id;
      },

      deleteTask(taskId) {
      check(taskId, String); // parameter type validation
      Tasks.remove(taskId);
      },

      setDone(taskId, done) {
      check(taskId, String); // parameter type validation
      check(done, Boolean); // parameter type validation
      Tasks.update(taskId, {$set: {done}});
      }
      });
    • Create the file client/util.js containing the following error handling function:

      export function handleError(err) {
      // Replace this will better error handling later.
      if (err) alert(err.message);
      }
    • Add the following import in client/App.svelte and client/Task.svelte:

      import {handleError} from './util.js';
    • Replace the call to Tasks.insert in the addTask function of client/App.svelte with the following:

      Meteor.call('addTask', text, handleError);
    • Remove the import of Tasks from client/Task.svelte since it is no longer used and add the following import:

      import {Meteor} from 'meteor/meteor';
    • Replace the call to Tasks.remove in the deleteTask function of client/Task.svelte with the following:

      Meteor.call('deleteTask', task._id, handleError);
    • Replace the call to Tasks.update in the toggleDone function of client/Task.svelte with the following:

      Meteor.call('setDone', task._id, !task.done, handleError);

    Nothing will change in the UI. But client-side code can no longer directly modify the MongoDB database.

  28. Explicitly specify what data the server sends to the client so we can separate tasks by user.

    Tasks for all users will still be stored in the same MongoDB collection, but each user will only see and operate on the tasks they add.

    • Remove the ability for the server to send any MongoDB content requested by clients. Currently the app includes the autopublish package. It shares every collection and every document in them with every connected client. This is convenient for prototyping, but not desired for production.

      Enter meteor remove autopublish.

    • Publish only tasks that belong to the logged in user by adding the following in imports/tasks.js after the line that starts with export const Tasks.

      if (Meteor.isServer) {
      // This is only run on the server.
      // An arrow function cannot be used here
      // because we need to use the "this" keyword.
      // If no query is provided, the entire collection is published.
      // A second argument can be passed to the find method which is
      // an object where the keys are names of properties.
      // The values are booleans that either specify which properties
      // to include (true) or which to exclude (false), but not both.
      Meteor.publish('tasks', function () {
      return Tasks.find({owner: this.userId});
      });
      }
    • Add the following import in client/App.svelte:

      import {onMount} from 'svelte';
    • Add the following in the script element of client/App.svelte to subscribe to all tasks that are published:

      // The name passed to subscribe must match a name passed to publish.
      onMount(() => Meteor.subscribe('tasks'));

      Additional arguments can be passed to Meteor.subscribe. These are passed to the function that is passed to Meteor.publish in the server and it can uses them to decide which documents to publish. This is useful to limit the amount of data that is copied into Minimongo on the client.

      The Meteor.subscribe method returns an object that can be captured in a variable. It has three properties. stop is a method that can be called to cancel the subscription. Typically the subscription is retained for the duration of the session. ready is a method returns the boolean true when the server has completed the initial publication of data. subscriptionId is a unique id for the subscription that is not typically needed.

    Now users only see and operate on their own tasks.

  29. Require account email validation before users can view and operate on their tasks.

    • Add the following at the bottom of server/accounts-setup.js:

      Accounts.onLogin(({user}) => {
      const [email] = user.emails;
      if (!email.verified) {
      Accounts.sendVerificationEmail(user._id, email.address);
      }
      });

      Accounts.emailTemplates.verifyEmail = {
      subject() {
      return `Activate your ${siteName} account.`;
      },
      text(user, url) {
      return (
      `Hey ${user.username}, click the following link ` +
      `to verify your email address: ${url}.`
      );
      }
      };
    • Determine if the logged in user has a verified email address by adding the following in client/App.svelte after the $: user = line:

      $: emailVerified = $user && $user.emails[0].verified;
    • Change {#if $user} to {#if $user && emailVerified}

    • Add the following before the {:else} line:

      {:else if $user && !emailVerified}
      <p>
      You have been sent an email containing a link to verify your account.
      Please click that link in order to start adding tasks.
      </p>

    Now after a user creates a new account, they will see the following:

    Todo App verify email sent

    The user must must verify their email address by clicking a link in an email they receive like the following before they can start adding tasks.

    Todo App verify email

    Clicking the link in the email opens a browser tab containing the following:

    Todo App email verified

    The user can dismiss the dialog and proceed with using the app.

That's it! We now have a functioning Todo app that supports user accounts and tasks are persisted in a database. I can't imagine doing all of this in less code using anything other than Meteor!

Routing

For apps that need client-side page routing there are two popular libraries:

Typically these are used with the Blaze framework. When using a different web framework such a Svelte or React, routing approaches for those can be used instead.

Meteor Shell

The meteor shell command launches a REPL that is used to examine and interact with the server side of a Meteor app. TODO: How is it used?

Building and Deploying

Now that we have our app running locally, we are ready to deploy it to a server so real users can access it.

MongoDB Hosting

If your Meteor app uses MongoDB, and most do, the first step is to select a hosting platform. One option is MongoDB Atlas. Another option is mLab.

MongoDB Atlas has an endless free tier referred to as "Shared Clusters" that does not provide backup.

To configure MongoDB Atlas:

To view databases and collections:

To connect from the mongo shell:

To connect from the MongoDB Compass app:

For more detail on using MongoDB Atlas with Meteor, see https://medium.com/@cfnelson/mongodb-atlas-with-meteor-a-step-by-step-guide-da34093665f4.

Configuring a Cloud Server

There are many options to choose from including AWS, Google Cloud Platform (GCP), and Azure.

An option that is fairly simple with a low cost is DigitalOcean.

You will want to associate a domain name with your DigitalOcean server so you can enable SSL encryption later. There are many domain registrars to choose from. A popular, low cost option is hover.com. You can also get a free domain name from freenom.com.

To associate a domain name with a DigitalOcean project, browse cloud.digitalocean.com, sign in, click "Networking" in the left nav., enter a domain for a project, and press "Add Domain". Then under "Create new record", select "A", enter "@" for "HOSTNAME", enter your DigitalOcean server IP address for "WILL DIRECT TO", and press "Create Record". Repeat this using "www" instead of "@" for "HOSTNAME". You can also create A records from the domain registrar site such as Hover.

After ssh'ing to your instance for the first time, it is helping to edit your .bashrc file. Some things you may want to add include:

# Make it easier to clear the screen.
alias cls='clear'

# Ask for confirmation before overwriting or deleting files.
alias cp="cp -i"
alias mv="mv -i"
alias rm="rm -i"

# Make the delete key work.
export TERM=xterm-256color

# Set environment variables that the Meteor server will need.
export MONGO_URL='{your-mongodb-url}'
export ROOT_URL=https://{ip-address-of-this-server}:{port-of-meteor-server}

To find processes that are listening on ports, enter netstat -tulpn. My DigitalOcean server has nginx listening on port 80 and node listening on port 3000. A different port can be used for the Meteor server.

To enable accessing you MongoDB Atlas server:

TODO: mup works now! Try to get it to use HTTPS! TODO: Get Galaxy to work. TODO: Get manual deploy to work.

Enabling HTTPS

To determine the version of Ubuntu you are using, enter lsb_release -a. For me this is 20.04.

DigitalOcean provides an nginx proxy that is started by default.

To enable nginx to act as a proxy for a server that listens on a particular port:

Setup nginx "server blocks" by following the steps here which are summaries below:

  1. Enter sudo mkdir -p /var/www/{your-domain}/html

  2. Enter sudo chown -R $USER:$USER /var/www/{your-domain}/html

  3. Enter sudo chmod -R 755 /var/www/{your-domain}

  4. Enter vim /var/www/{your-domain}/html/index.html

  5. Add content like the following for testing your site:

    <html>
    <head>
    <title>Welcome to {your-site}!</title>
    </head>
    <body>
    <h1>Success, the {your-site} server block is working!</h1>
    </body>
    </html>
  6. Enter sudo vim /etc/nginx/sites-available/{your-domain}

  7. Add content like the following to serve your site:

    server {
    listen 80;
    listen [::]:80;

    root /var/www/your_domain/html;
    index index.html index.htm index.nginx-debian.html;

    server_name your_domain www.your_domain;

    location / {
    try_files $uri $uri/ =404;
    }
    }
  8. Enter sudo ln -s /etc/nginx/sites-available/{your-domain} /etc/nginx/sites-enabled/

  9. Enter sudo vim /etc/nginx/nginx.conf

  10. Uncomment the line server_names_hash_bucket_size 64;

  11. Enter sudo nginx -t to verify that there are no syntax errors in the edited files.

  12. Enter sudo systemctl restart nginx to restart nginx.

  13. Browse http://{your-domain} to verify that nginx can server the earlier HTML.

Enable SSL by following the steps here. These instructions specific to Ubuntu version 20.04. It uses a "certbot" which will automatically renew the certificate before it expires.

  1. Enter sudo apt install python3-certbot-nginx
  2. Enter sudo ufw status to see that network traffic is currently allowed.
  3. If "Nginx FULL" is not set to "ALLOW" ...
    • Enter sudo ufw allow 'Nginx Full'
    • Enter sudo ufw delete allow 'Nginx HTTP' because it is implied by Nginx Full.
  4. Enter cd to change to your home directory.
  5. Enter sudo certbot --nginx -d {your-site} -d www.{your-site} to obtain an SSL certificate. You will be prompted to enter your email, agree to the LetsEncrypt terms of service, optionally receive email from the Electronic Freedom Foundation, and whether HTTP requests should be redirected to HTTPS.

Your certificate and chain are saved at /etc/letsencrypt/live/{your-site}/fullchain.pem. Your key file is saved at /etc/letsencrypt/live/mvolkmann.site/privkey.pem. Normally your cert will expire in three months any you need to obtain a new version by running the certbot command again with the --certonly option. You can also non-interactively renew all of your certificates by entering certbot renew. But certbot makes this unnecessary and will automatically renew your cert. Make a backup of the /etc/letsencrypt directory.

You can now browse https://{your-domain}.

Manual Deploy

To build a Meteor app for deployment:

This creates the specified directory if it doesn't exist and creates a TAR file named {project-name}.tar.gz inside it. The TAR file contains a single directory named bundle which contains all the files for the client-side and server-side of the application.

For help on the build command, enter meteor help build.

To deploy the app:

  1. Copy the TAR file to a server, perhaps using the scp command. For example, scp {project-name}.tar.gz root@{server-ip-address}:.
  2. Start a terminal session on the server using the ssh command. For example, ssh root@{server-ip-address}. (I store the IP address and password in 1Password.)
  3. Verify that a recent version of Node.js is installed by entering node -v.
  4. Create a directory to receive the contents of the tar file by entering mkdir {project-name}
  5. Un-tar the file by entering tar zxf {project-name}.tar.gz -C {project-name}
  6. Enter cd {project-name}/bundle/programs/server
  7. Enter npm install
  8. Enter cd ../..
  9. Set the MONGO_URL environment variable to your MongoDB server URL. For example, mongodb+srv://{username}:{password}@{mongo-host}/meteor
  10. Set the ROOT_URL environment variable to your HTTP server URL. For example, http://{server-ip-address}:{server-port} For example, http://{server-ip-address}:4000
  11. Expose the port that will be used. With DigitalOcean this is done by entering sudo ufw allow {port}/tcp. Meteor uses port 3000 by default, but my DigitalOcean instance has an HTTP server that is always listening on that port, so choose another such as 4000.
  12. Enter node main.js to start the server. TODO: Do you need to do something to tell it to use port 4000 TODO: or does it get that from the ROOT_URL environment variable?

This gives the error MongoNetworkError: failed to connect to server [cluster0-shard-00-00.tqq7g.mongodb.net:27017] on first connect [MongoNetworkError: connection 5 to cluster0-shard-00-00.tqq7g.mongodb.net:27017 closed

Other alternatives for deploying Meteor apps include Galaxy (here) and Meteor Up (here). Meteor Up, a.k.a mup, can be used to deploy a Meteor app to any server to which you can ssh.

Meteor Up (mup)

Meteor Up uses Docker to deploy Meteor apps.

The steps to deploy a Meteor app using Meteor Up are:

If the last step is successful, browse the URL in app.env.ROOT_URL. This defaults to use a MongoDB database named "test", not "meteor".

Galaxy

Galaxy provides a free, one-month trial.

Galaxy does not provided MongoDB hosting.

To use Galaxy:

This will output the following:

Talking to Galaxy servers at {some-url}
Preparing to deploy your app...
Uploaded app bundle to upgrade {username}.meteorapp.com to version 3.
Galaxy is building the app into a native image.
Building app image...
Deploying app...

The last step will fail if it cannot connect to the MongoDB server. Note that Galaxy-provided servers have dynamic IP addresses. For this reason, the MongoDB server must accept requests from an IP address range, not specific IP addresses. The free tier of MongoDB Atlas does not support whitelisting IP address ranges, so upgrading to a paid tier is required to connect to it from a Galaxy server.

Creating Meteor Packages

Early in the "Meteor Packages" section we discussed Isopacks which are packages containing code that often can run on the client or server. You can create your own Isopacks. They can existing locally, only for use by your applications. You can also publish them so anyone can use them, similar to npm packages.

To create an Isopack:

  1. Register as a Meteor developer. Your username will be the prefix be used to identify packages you author.
  2. Enter meteor create --package {your-prefix}:{package-name}
    This creates a directory using the package name containing the files README.md (empty), {package-name}.js, {package-name}-tests.js, and package.js.
  3. Edit {package-name}.js to add functions. Keep the export of the constant name. Add exports of functions, classes, and other values.
  4. Edit {package-name}-test.js to add tests for code in {package-name}.js.
    Follow the naming convention used in the example test provided for the package name. The test object passed to each test function supports the following methods for making assertions: equal, notEqual, instanceOf, isTrue, isFalse, isNull, isNotNull, isUndefined, isNan, matches, and length.
  5. Run tests by entering meteor test-packages ./.
    The names of tests run in the server are preceded by "S:". Those run in the client are preceded by "C:".
  6. Browse localhost:3000 to see test results.
  7. Modify code until tests pass. Tests rerun automatically when changes are detected and browser refreshes.
  8. Create a Git repository for the package.
  9. Push the code to the Git repository.
  10. Modify package.js to set version and add summary and git URL.
  11. Publish the package by entering meteor publish --create.
    It can take 5-10 minutes for the package to appear on atmosphere.js, but it should be found by meteor search immediately.
  12. To publish changes after the initial publish, bump the version in package.js, push changes to the Git repository, and enter meteor publish. These changes will appear on atmosphere.js immediately.

See the earlier "Meteor Packages" section for details on adding the use of packages to a project and importing values from them.

Building a Mobile App

Resources