diff --git a/api/games/games.ts b/api/games/games.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a967dc798eb71dabe51627af8d1f8fad031401da
--- /dev/null
+++ b/api/games/games.ts
@@ -0,0 +1,187 @@
+import { Mongo } from 'meteor/mongo'
+import SimpleSchema from 'simpl-schema'
+import { ValidatedMethod } from 'meteor/mdg:validated-method'
+import { loggedInMixin, schemaMixin } from '../mixins'
+import { Meteor } from 'meteor/meteor'
+import { Card } from './cards'
+import { Bet, GameSystem } from './logic'
+
+export const Games = new Mongo.Collection('games')
+
+export function notInGameMixin<A, R> (method: ValidatedMethod<A, R>) {
+  let run = method.run
+  method.run = function (...params: any[]) {
+    if (Games.find({'players.id': this.userId}).count() > 0) throw new Meteor.Error('already_in_game')
+    return run.call(this, ...params)
+  }
+  return method
+}
+
+export function inGameMixin<A, R> (method: ValidatedMethod<A, R>) {
+  let run = method.run
+  method.run = function (...params: any[]) {
+    this.game = Games.findOne({'players.id': this.userId})
+    if (this.game == null) throw new Meteor.Error('not_in_game')
+    this.game.sys = GameSystem.fromObject(this.game.sys)
+    return run.call(this, ...params)
+  }
+  return method
+}
+
+const PlayerSchema = new SimpleSchema({
+  id: String,
+  position: {type: Number, allowedValues: [0, 1, 2, 3]}
+})
+
+const GameSchema = new SimpleSchema({
+  name: String,
+  players: {type: Array, defaultValue: [], optional: true},
+  'players.$': PlayerSchema,
+  sys: {type: GameSystem, defaultValue: new GameSystem(), optional: true}
+})
+
+interface Player {
+  id: string
+  position: number
+}
+
+interface Game {
+  _id: string
+  name: string
+  players: Player[]
+  sys: GameSystem
+}
+
+export const newGame = new ValidatedMethod<{ name: string }, string>({
+  name: 'Game.new',
+  mixins: [schemaMixin, loggedInMixin, notInGameMixin],
+  validate: new SimpleSchema({
+    name: String
+  }),
+  run (game) {
+    if (Games.find(game).count() === 0) {
+      let gameClean: Game = GameSchema.clean(game) as Game
+      gameClean.sys = gameClean.sys.toObject()
+      return Games.insert(gameClean)
+    } else {
+      throw new Meteor.Error('name_taken')
+    }
+  }
+})
+
+export const joinGame = new ValidatedMethod<{ _id: string }, void>({
+  name: 'Game.join',
+  mixins: [schemaMixin, loggedInMixin, notInGameMixin],
+  validate: new SimpleSchema({
+    _id: String
+  }),
+  run ({_id}) {
+    let game = Games.findOne({_id}) as Game
+    if (game == null) throw new Meteor.Error('game_not_found')
+    if (game.players.length === 4) throw new Meteor.Error('game_full')
+    game.players.push({id: this.userId, position: game.players.length})
+    if (game.players.length === 4) {
+      game.sys = GameSystem.fromObject(game.sys)
+      game.sys.newRound()
+      game.sys = game.sys.toObject()
+    }
+    Games.update({_id}, {$set: game})
+  }
+})
+
+export const nextCards = new ValidatedMethod<{}, void>({
+  name: 'Game.nextCards',
+  mixins: [schemaMixin, loggedInMixin, inGameMixin],
+  validate: new SimpleSchema({}),
+  run () {
+    callSystem(this, this.game.sys.nextCards)
+  }
+})
+
+export const giveCards = new ValidatedMethod<{ cards: Card[] }, void>({
+  name: 'Game.giveCards',
+  mixins: [schemaMixin, loggedInMixin, inGameMixin],
+  validate: new SimpleSchema({
+    cards: {type: Array, minCount: 3, maxCount: 3},
+    'cards.$': Card
+  }),
+  run ({cards}) {
+    callSystem(this, this.game.sys.giveCards, cards)
+  }
+})
+
+export const playCards = new ValidatedMethod({
+  name: 'Game.playCards',
+  mixins: [schemaMixin, loggedInMixin, inGameMixin],
+  validate: new SimpleSchema({
+    cards: [Card]
+  }),
+  run ({cards}) {
+    let player = this.game.players.findIndex(p => p.id === this.userId)
+
+    if (!this.game.sys.playCards(player, cards)) {
+      throw new Meteor.Error('playCards')
+    }
+
+    let winner = this.game.sys.winner()
+
+    if (winner >= 0) {
+      Games.remove({_id: this.game._id})
+      return winner
+    } else {
+      this.game.sys = this.game.sys.toObject()
+      Games.update({_id: this.game._id}, {$set: this.game})
+    }
+  }
+})
+
+export const skipTurn = new ValidatedMethod<{}, void>({
+  name: 'Game.skipTurn',
+  mixins: [schemaMixin, loggedInMixin, inGameMixin],
+  validate: new SimpleSchema({}),
+  run () {
+    callSystem(this, this.game.sys.skipTurn)
+  }
+})
+
+export const giveDragon = new ValidatedMethod<{ to: number }, void>({
+  name: 'game.giveDragon',
+  mixins: [schemaMixin, loggedInMixin, inGameMixin],
+  validate: new SimpleSchema({
+    to: {type: Number, allowedValues: [0, 1]}
+  }),
+  run ({to}) {
+    callSystem(this, this.game.sys.giveDragon, to)
+  }
+})
+
+export const makeCall = new ValidatedMethod<{ bet: number }, void>({
+  name: 'Game.makeCall',
+  mixins: [schemaMixin, loggedInMixin, inGameMixin],
+  validate: new SimpleSchema({
+    bet: {type: Number, allowedValues: [Bet.TICHU, Bet.GRAND]}
+  }),
+  run ({bet}) {
+    callSystem(this, this.game.sys.makeCall, bet)
+  }
+})
+
+function newGameMethod (name: string, schema: SimpleSchema, argName?: string): ValidatedMethod<any, void> {
+  return new ValidatedMethod<any, void>({
+    name: 'Game.' + name,
+    mixins: [schemaMixin, loggedInMixin, inGameMixin],
+    validate: schema,
+    run (arg) {
+      callSystem(this, this.game.sys[name], argName ? arg[argName] : null)
+    }
+  })
+}
+
+function callSystem<T> (self: { game: Game, userId: string }, method: (player: number, arg?: T) => boolean, arg?: T): void {
+  let player = self.game.players.findIndex(p => p.id === self.userId)
+  if (!method(player, arg)) {
+    throw new Meteor.Error(method.name)
+  }
+  let sysObject = self.game.sys.toObject()
+  Games.update({_id: self.game._id}, {$set: sysObject})
+}